@dataparade/cli 0.0.6 → 0.0.8

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 (71) hide show
  1. package/README.md +3 -2
  2. package/dist/package.json +2 -2
  3. package/dist/src/ai-enrichment/providers/families/chat-completions-family.js +4 -1
  4. package/dist/src/ai-enrichment/providers/families/generate-content-family.js +3 -1
  5. package/dist/src/ai-enrichment/providers/families/messages-family.js +1 -1
  6. package/dist/src/ai-enrichment/providers/families/ollama-generate-family.js +1 -1
  7. package/dist/src/ai-enrichment/providers/types.d.ts +6 -0
  8. package/dist/src/cli.js +85 -41
  9. package/dist/src/config/env.js +7 -2
  10. package/dist/src/config/load-cli-env.d.ts +2 -0
  11. package/dist/src/config/load-cli-env.js +11 -2
  12. package/dist/src/config/redact.js +3 -0
  13. package/dist/src/config/validate-scan-ai.js +13 -7
  14. package/dist/src/core/pipeline/ai-orchestrator-options.js +8 -4
  15. package/dist/src/core/pipeline/orchestrator.js +1 -1
  16. package/dist/src/core/schema/scan-config.schema.d.ts +1 -0
  17. package/dist/src/core/schema/scan-config.schema.js +1 -0
  18. package/dist/src/core/types/config.d.ts +5 -0
  19. package/dist/src/platform-api/anonymous-ai-session-client.d.ts +14 -0
  20. package/dist/src/platform-api/anonymous-ai-session-client.js +40 -0
  21. package/dist/src/platform-api/read-cli-api-error-message.d.ts +18 -0
  22. package/dist/src/platform-api/read-cli-api-error-message.js +31 -0
  23. package/dist/src/platform-api/scan-quota-client.d.ts +5 -0
  24. package/dist/src/platform-api/scan-quota-client.js +24 -14
  25. package/dist/src/platform-api/upload-client.d.ts +10 -3
  26. package/dist/src/platform-api/upload-client.js +35 -16
  27. package/dist/src/platform-api/upload.types.d.ts +10 -5
  28. package/dist/src/upload/build-preview-url.d.ts +1 -0
  29. package/dist/src/upload/build-preview-url.js +6 -0
  30. package/dist/src/upload/run-upload.js +20 -4
  31. package/dist/src/upload/types.d.ts +2 -2
  32. package/dist/tests/cli/cli.spec.js +17 -3
  33. package/dist/tests/unit/ai-enrichment/provider-prompt-backend-contract.spec.d.ts +1 -0
  34. package/dist/tests/unit/ai-enrichment/provider-prompt-backend-contract.spec.js +134 -0
  35. package/dist/tests/unit/ai-enrichment/providers/system-prompt-override.spec.d.ts +1 -0
  36. package/dist/tests/unit/ai-enrichment/providers/system-prompt-override.spec.js +91 -0
  37. package/dist/tests/unit/cli/scan-command-ai-flags.spec.js +19 -0
  38. package/dist/tests/unit/cli/scan-command-diagram-graph.spec.js +10 -1
  39. package/dist/tests/unit/cli/scan-command-e2e-monorepo-app-plus-terraform.spec.js +12 -8
  40. package/dist/tests/unit/cli/scan-command-e2e-monorepo-front-back-sections.spec.js +10 -1
  41. package/dist/tests/unit/cli/scan-command-e2e-python-basic.spec.js +10 -1
  42. package/dist/tests/unit/cli/scan-command-e2e-terraform-basic.spec.js +12 -8
  43. package/dist/tests/unit/cli/scan-command-e2e-ts-sample.spec.js +10 -1
  44. package/dist/tests/unit/cli/scan-command-e2e-ts-sentry-route-env.spec.js +1 -1
  45. package/dist/tests/unit/cli/scan-command-exit-code.spec.js +23 -0
  46. package/dist/tests/unit/cli/scan-quota-flow.spec.js +44 -1
  47. package/dist/tests/unit/config/load-cli-env.spec.d.ts +1 -0
  48. package/dist/tests/unit/config/load-cli-env.spec.js +52 -0
  49. package/dist/tests/unit/config/redact.spec.js +1 -1
  50. package/dist/tests/unit/config/resolve-config.spec.js +1 -1
  51. package/dist/tests/unit/config/validate-scan-ai.spec.js +25 -0
  52. package/dist/tests/unit/core/ai-orchestrator-options-anon.spec.d.ts +1 -0
  53. package/dist/tests/unit/core/ai-orchestrator-options-anon.spec.js +29 -0
  54. package/dist/tests/unit/core/dependency-manifest-performance-budgets.spec.js +1 -1
  55. package/dist/tests/unit/core/graph-mapping.spec.js +1 -1
  56. package/dist/tests/unit/core/invariant-tests-dedupe-determinism.spec.js +1 -1
  57. package/dist/tests/unit/core/no-source-code-leakage.spec.js +1 -1
  58. package/dist/tests/unit/core/orchestrator.spec.js +12 -12
  59. package/dist/tests/unit/core/output-json.spec.js +2 -2
  60. package/dist/tests/unit/core/parsing-error-handling-nonfatal.spec.js +1 -1
  61. package/dist/tests/unit/core/scan-result-language-stats.spec.js +1 -1
  62. package/dist/tests/unit/core/structural-scan.spec.js +1 -1
  63. package/dist/tests/unit/core/terraform-json-overlay.spec.js +1 -1
  64. package/dist/tests/unit/platform-api/anonymous-ai-session-client.spec.d.ts +1 -0
  65. package/dist/tests/unit/platform-api/anonymous-ai-session-client.spec.js +35 -0
  66. package/dist/tests/unit/platform-api/read-cli-api-error-message.spec.d.ts +1 -0
  67. package/dist/tests/unit/platform-api/read-cli-api-error-message.spec.js +38 -0
  68. package/dist/tests/unit/upload/build-preview-url.spec.js +16 -0
  69. package/dist/tests/unit/upload/run-upload.spec.d.ts +1 -0
  70. package/dist/tests/unit/upload/run-upload.spec.js +43 -0
  71. package/package.json +2 -2
@@ -1,9 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CliScanQuotaExceededError = void 0;
3
+ exports.CliScanAlreadyRunningError = exports.CliScanQuotaExceededError = void 0;
4
4
  exports.cliScanPreflight = cliScanPreflight;
5
5
  exports.cliScanComplete = cliScanComplete;
6
6
  const dataparade_api_base_url_1 = require("./dataparade-api-base-url");
7
+ const read_cli_api_error_message_1 = require("./read-cli-api-error-message");
7
8
  /** Thrown when preflight/complete is rejected with HTTP 403 scan_quota_exceeded. */
8
9
  class CliScanQuotaExceededError extends Error {
9
10
  constructor(message) {
@@ -13,6 +14,15 @@ class CliScanQuotaExceededError extends Error {
13
14
  }
14
15
  }
15
16
  exports.CliScanQuotaExceededError = CliScanQuotaExceededError;
17
+ /** Thrown when preflight is rejected with HTTP 409 scan_already_running. */
18
+ class CliScanAlreadyRunningError extends Error {
19
+ constructor(message) {
20
+ super(message);
21
+ this.code = "scan_already_running";
22
+ this.name = "CliScanAlreadyRunningError";
23
+ }
24
+ }
25
+ exports.CliScanAlreadyRunningError = CliScanAlreadyRunningError;
16
26
  function buildHeaders(apiKey) {
17
27
  return {
18
28
  Authorization: `Bearer ${apiKey}`,
@@ -31,15 +41,14 @@ async function cliScanPreflight(input) {
31
41
  }),
32
42
  });
33
43
  if (!res.ok) {
34
- let message = `Scan preflight failed (${res.status})`;
44
+ const fallback = `Scan preflight failed (${res.status})`;
35
45
  let code;
46
+ let message = fallback;
36
47
  try {
37
- const body = (await res.json());
38
- code = body.code;
39
- if (typeof body.message === "string")
40
- message = body.message;
41
- else if (Array.isArray(body.message))
42
- message = body.message.join(", ");
48
+ const body = await res.json();
49
+ const parsed = (0, read_cli_api_error_message_1.readCliApiErrorMessage)(body, fallback);
50
+ code = parsed.code;
51
+ message = parsed.message;
43
52
  }
44
53
  catch {
45
54
  // ignore
@@ -47,6 +56,9 @@ async function cliScanPreflight(input) {
47
56
  if (res.status === 403 && code === "scan_quota_exceeded") {
48
57
  throw new CliScanQuotaExceededError(message);
49
58
  }
59
+ if (res.status === 409 && code === "scan_already_running") {
60
+ throw new CliScanAlreadyRunningError(message);
61
+ }
50
62
  throw new Error(message);
51
63
  }
52
64
  return (await res.json());
@@ -63,13 +75,11 @@ async function cliScanComplete(input) {
63
75
  }),
64
76
  });
65
77
  if (!res.ok) {
66
- let message = `Scan complete report failed (${res.status})`;
78
+ const fallback = `Scan complete report failed (${res.status})`;
79
+ let message = fallback;
67
80
  try {
68
- const body = (await res.json());
69
- if (typeof body.message === "string")
70
- message = body.message;
71
- else if (Array.isArray(body.message))
72
- message = body.message.join(", ");
81
+ const body = await res.json();
82
+ message = (0, read_cli_api_error_message_1.readCliApiErrorMessage)(body, fallback).message;
73
83
  }
74
84
  catch {
75
85
  // ignore
@@ -1,3 +1,10 @@
1
- import type { CliUploadPreviewInput, CliUploadPreviewResponse } from "./upload.types";
2
- export type { CliUploadPreviewInput, CliUploadPreviewResponse } from "./upload.types";
3
- export declare function cliUploadPreview(input: CliUploadPreviewInput): Promise<CliUploadPreviewResponse>;
1
+ import type { CliAnonymousUploadPreviewResponse, CliUploadPreviewInput, CliUploadPreviewResponse } from "./upload.types";
2
+ export type { CliAnonymousUploadPreviewResponse, CliUploadPreviewInput, CliUploadPreviewResponse, } from "./upload.types";
3
+ export declare function cliUploadAnonymousPreview(input: {
4
+ dataflow: unknown;
5
+ projectName?: string;
6
+ scanJobId?: string;
7
+ }): Promise<CliAnonymousUploadPreviewResponse>;
8
+ export declare function cliUploadPreview(input: CliUploadPreviewInput & {
9
+ apiKey: string;
10
+ }): Promise<CliUploadPreviewResponse>;
@@ -1,17 +1,49 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cliUploadAnonymousPreview = cliUploadAnonymousPreview;
3
4
  exports.cliUploadPreview = cliUploadPreview;
4
5
  const dataparade_api_base_url_1 = require("./dataparade-api-base-url");
5
- function buildHeaders(apiKey) {
6
+ function buildAuthHeaders(apiKey) {
6
7
  return {
7
8
  Authorization: `Bearer ${apiKey}`,
8
9
  "Content-Type": "application/json",
9
10
  };
10
11
  }
12
+ async function parseUploadError(res) {
13
+ let message = `Upload failed (${res.status})`;
14
+ try {
15
+ const body = (await res.json());
16
+ if (typeof body.message === "string")
17
+ message = body.message;
18
+ else if (Array.isArray(body.message))
19
+ message = body.message.join(", ");
20
+ else if (typeof body.error === "string")
21
+ message = body.error;
22
+ }
23
+ catch {
24
+ // ignore
25
+ }
26
+ throw new Error(message);
27
+ }
28
+ async function cliUploadAnonymousPreview(input) {
29
+ const res = await fetch(`${(0, dataparade_api_base_url_1.getDataparadeApiBaseUrl)()}/api/scans/cli/preview`, {
30
+ method: "POST",
31
+ headers: { "Content-Type": "application/json" },
32
+ body: JSON.stringify({
33
+ dataflow: input.dataflow,
34
+ projectName: input.projectName,
35
+ scanJobId: input.scanJobId,
36
+ }),
37
+ });
38
+ if (!res.ok) {
39
+ return await parseUploadError(res);
40
+ }
41
+ return (await res.json());
42
+ }
11
43
  async function cliUploadPreview(input) {
12
44
  const res = await fetch(`${(0, dataparade_api_base_url_1.getDataparadeApiBaseUrl)()}/api/scans/cli/upload`, {
13
45
  method: "POST",
14
- headers: buildHeaders(input.apiKey),
46
+ headers: buildAuthHeaders(input.apiKey),
15
47
  body: JSON.stringify({
16
48
  dataflow: input.dataflow,
17
49
  projectName: input.projectName,
@@ -19,20 +51,7 @@ async function cliUploadPreview(input) {
19
51
  }),
20
52
  });
21
53
  if (!res.ok) {
22
- let message = `Upload failed (${res.status})`;
23
- try {
24
- const body = (await res.json());
25
- if (typeof body.message === "string")
26
- message = body.message;
27
- else if (Array.isArray(body.message))
28
- message = body.message.join(", ");
29
- else if (typeof body.error === "string")
30
- message = body.error;
31
- }
32
- catch {
33
- // ignore
34
- }
35
- throw new Error(message);
54
+ return await parseUploadError(res);
36
55
  }
37
56
  return (await res.json());
38
57
  }
@@ -1,10 +1,15 @@
1
+ export type CliUploadPreviewResponse = {
2
+ draftId: string;
3
+ projectName?: string | null;
4
+ };
5
+ export type CliAnonymousUploadPreviewResponse = {
6
+ claimToken: string;
7
+ expiresAt: string;
8
+ projectName: string;
9
+ };
1
10
  export type CliUploadPreviewInput = {
2
- apiKey: string;
11
+ apiKey?: string;
3
12
  dataflow: unknown;
4
13
  projectName?: string;
5
14
  scanJobId?: string;
6
15
  };
7
- export type CliUploadPreviewResponse = {
8
- draftId: string;
9
- projectName?: string | null;
10
- };
@@ -1 +1,2 @@
1
1
  export declare function buildImportPreviewUrl(draftId: string): string;
2
+ export declare function buildAnonymousCliPreviewUrl(claimToken: string): string;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildImportPreviewUrl = buildImportPreviewUrl;
4
+ exports.buildAnonymousCliPreviewUrl = buildAnonymousCliPreviewUrl;
4
5
  const dataparade_app_base_url_1 = require("../platform-api/dataparade-app-base-url");
5
6
  function buildImportPreviewUrl(draftId) {
6
7
  const base = (0, dataparade_app_base_url_1.getDataparadeAppBaseUrl)();
@@ -8,3 +9,8 @@ function buildImportPreviewUrl(draftId) {
8
9
  url.searchParams.set("importDraft", draftId);
9
10
  return url.toString();
10
11
  }
12
+ function buildAnonymousCliPreviewUrl(claimToken) {
13
+ const base = (0, dataparade_app_base_url_1.getDataparadeAppBaseUrl)();
14
+ const encoded = encodeURIComponent(claimToken);
15
+ return `${base}/preview/cli/${encoded}`;
16
+ }
@@ -5,14 +5,30 @@ const upload_client_1 = require("../platform-api/upload-client");
5
5
  const build_preview_url_1 = require("./build-preview-url");
6
6
  async function runDataflowUpload(input) {
7
7
  const prefix = input.logPrefix ?? "[upload]";
8
- const result = await (0, upload_client_1.cliUploadPreview)({
9
- apiKey: input.apiKey,
8
+ if (input.apiKey?.trim()) {
9
+ const result = await (0, upload_client_1.cliUploadPreview)({
10
+ apiKey: input.apiKey.trim(),
11
+ dataflow: input.dataflow,
12
+ projectName: input.projectName,
13
+ scanJobId: input.scanJobId,
14
+ });
15
+ const previewUrl = (0, build_preview_url_1.buildImportPreviewUrl)(result.draftId);
16
+ // eslint-disable-next-line no-console
17
+ console.log(`${prefix} Preview ready: ${previewUrl}`);
18
+ return { draftId: result.draftId, previewUrl };
19
+ }
20
+ const result = await (0, upload_client_1.cliUploadAnonymousPreview)({
10
21
  dataflow: input.dataflow,
11
22
  projectName: input.projectName,
12
23
  scanJobId: input.scanJobId,
13
24
  });
14
- const previewUrl = (0, build_preview_url_1.buildImportPreviewUrl)(result.draftId);
25
+ const previewUrl = (0, build_preview_url_1.buildAnonymousCliPreviewUrl)(result.claimToken);
15
26
  // eslint-disable-next-line no-console
16
27
  console.log(`${prefix} Preview ready: ${previewUrl}`);
17
- return { draftId: result.draftId, previewUrl };
28
+ // eslint-disable-next-line no-console
29
+ console.log(`${prefix} Claiming this preview after signup uses 1 scan slot` +
30
+ (input.scanJobId
31
+ ? " and any platform AI tokens used during the scan."
32
+ : "."));
33
+ return { previewUrl };
18
34
  }
@@ -1,11 +1,11 @@
1
1
  export type RunDataflowUploadInput = {
2
- apiKey: string;
2
+ apiKey?: string;
3
3
  dataflow: unknown;
4
4
  projectName?: string;
5
5
  scanJobId?: string;
6
6
  logPrefix?: string;
7
7
  };
8
8
  export type RunDataflowUploadResult = {
9
- draftId: string;
9
+ draftId?: string;
10
10
  previewUrl: string;
11
11
  };
@@ -14,7 +14,7 @@ describe("CLI", () => {
14
14
  }
15
15
  });
16
16
  it("scan <path> writes a dataflow.json wrapper to the current directory and exits 0", () => {
17
- const result = (0, child_process_1.spawnSync)("node", [cliDistPath, "scan", "."], {
17
+ const result = (0, child_process_1.spawnSync)("node", [cliDistPath, "scan", ".", "--no-ai-inference", "--skip-auto-upload"], {
18
18
  encoding: "utf-8",
19
19
  cwd: path_1.default.join(__dirname, "../../"),
20
20
  });
@@ -33,7 +33,13 @@ describe("CLI", () => {
33
33
  it("scan ../frontend produces a dataflow.json wrapper with at least one node", () => {
34
34
  const cliCwd = path_1.default.join(__dirname, "../../");
35
35
  const frontendPath = "../frontend";
36
- const result = (0, child_process_1.spawnSync)("node", [cliDistPath, "scan", frontendPath], {
36
+ const result = (0, child_process_1.spawnSync)("node", [
37
+ cliDistPath,
38
+ "scan",
39
+ frontendPath,
40
+ "--no-ai-inference",
41
+ "--skip-auto-upload",
42
+ ], {
37
43
  encoding: "utf-8",
38
44
  cwd: cliCwd,
39
45
  });
@@ -48,7 +54,15 @@ describe("CLI", () => {
48
54
  const cliCwd = path_1.default.join(__dirname, "../../");
49
55
  const frontendPath = "../frontend";
50
56
  const outputFile = "scan-frontend-summary.json";
51
- const result = (0, child_process_1.spawnSync)("node", [cliDistPath, "scan", frontendPath, "--output", outputFile], {
57
+ const result = (0, child_process_1.spawnSync)("node", [
58
+ cliDistPath,
59
+ "scan",
60
+ frontendPath,
61
+ "--output",
62
+ outputFile,
63
+ "--no-ai-inference",
64
+ "--skip-auto-upload",
65
+ ], {
52
66
  encoding: "utf-8",
53
67
  cwd: cliCwd,
54
68
  });
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const provider_prompt_1 = require("../../../src/ai-enrichment/provider-prompt");
4
+ /**
5
+ * Backend contract guard (DP-P0-CLI-3810).
6
+ *
7
+ * The backend validates every platform infer prompt against
8
+ * backend/src/scans/scan-cli-infer-prompt.schema.ts:
9
+ * - instructions: non-empty string
10
+ * - agent: tpAgent | propertyAgent | directionAgent | interactionAgent
11
+ * - candidates: non-empty array, each with id (string) + candidateType (enum)
12
+ * - componentContext: object
13
+ * - canonicalComponentIds: string[]
14
+ *
15
+ * If buildProviderPromptPayload stops satisfying this shape, platform AI scans
16
+ * will be rejected with 400 invalid_infer_prompt. Keep both sides in sync.
17
+ */
18
+ const AGENTS = [
19
+ "tpAgent",
20
+ "propertyAgent",
21
+ "directionAgent",
22
+ "interactionAgent",
23
+ ];
24
+ const CANDIDATE_TYPES = new Set([
25
+ "third_party",
26
+ "node_property",
27
+ "flow_direction",
28
+ "missing_interaction",
29
+ ]);
30
+ function makeComponent() {
31
+ return {
32
+ id: "tp_1",
33
+ name: "Acme API",
34
+ type: "third_party",
35
+ confidence: 0.9,
36
+ detectedFrom: [],
37
+ sourceLocations: [{ filePath: "src/lib/acme.ts", startLine: 1, endLine: 5 }],
38
+ properties: { vendor: null },
39
+ };
40
+ }
41
+ function makeCandidate() {
42
+ return {
43
+ id: "cand-1",
44
+ candidateType: "node_property",
45
+ priority: 1,
46
+ componentId: "tp_1",
47
+ missingFields: ["vendor"],
48
+ rationale: "sparse properties",
49
+ hints: [],
50
+ };
51
+ }
52
+ function assertMatchesBackendContract(payload) {
53
+ expect(typeof payload.instructions).toBe("string");
54
+ expect(payload.instructions.length).toBeGreaterThan(0);
55
+ expect(AGENTS).toContain(payload.agent);
56
+ expect(Array.isArray(payload.candidates)).toBe(true);
57
+ const candidates = payload.candidates;
58
+ expect(candidates.length).toBeGreaterThan(0);
59
+ for (const cand of candidates) {
60
+ expect(typeof cand.id).toBe("string");
61
+ expect(cand.id.length).toBeGreaterThan(0);
62
+ expect(CANDIDATE_TYPES.has(cand.candidateType)).toBe(true);
63
+ }
64
+ expect(payload.componentContext).toBeDefined();
65
+ expect(typeof payload.componentContext).toBe("object");
66
+ expect(Array.isArray(payload.componentContext)).toBe(false);
67
+ expect(Array.isArray(payload.canonicalComponentIds)).toBe(true);
68
+ for (const id of payload.canonicalComponentIds) {
69
+ expect(typeof id).toBe("string");
70
+ }
71
+ if (payload.relevantFileContents !== undefined) {
72
+ for (const value of Object.values(payload.relevantFileContents)) {
73
+ expect(typeof value).toBe("string");
74
+ }
75
+ }
76
+ // The wire format is JSON.stringify(payload); it must round-trip.
77
+ expect(() => JSON.parse(JSON.stringify(payload))).not.toThrow();
78
+ }
79
+ describe("buildProviderPromptPayload backend contract (DP-P0-CLI-3810)", () => {
80
+ it.each(AGENTS)("output for %s satisfies the backend infer schema", (agent) => {
81
+ const payload = (0, provider_prompt_1.buildProviderPromptPayload)({
82
+ agent,
83
+ queue: [makeCandidate()],
84
+ components: [makeComponent()],
85
+ dataFlows: [],
86
+ });
87
+ assertMatchesBackendContract(payload);
88
+ });
89
+ it("output with file excerpts still satisfies the contract", () => {
90
+ const payload = (0, provider_prompt_1.buildProviderPromptPayload)({
91
+ agent: "propertyAgent",
92
+ queue: [makeCandidate()],
93
+ components: [makeComponent()],
94
+ dataFlows: [],
95
+ files: [
96
+ {
97
+ path: "src/lib/acme.ts",
98
+ name: "acme.ts",
99
+ content: 'import { acme } from "acme";',
100
+ language: "typescript",
101
+ size: 30,
102
+ },
103
+ ],
104
+ });
105
+ assertMatchesBackendContract(payload);
106
+ });
107
+ it("flow-only candidates (empty componentContext) still satisfy the contract", () => {
108
+ const payload = (0, provider_prompt_1.buildProviderPromptPayload)({
109
+ agent: "directionAgent",
110
+ queue: [
111
+ {
112
+ id: "cand-flow",
113
+ candidateType: "flow_direction",
114
+ priority: 1,
115
+ flowId: "flow_1",
116
+ missingFields: [],
117
+ rationale: "ambiguous direction",
118
+ hints: [],
119
+ },
120
+ ],
121
+ components: [makeComponent()],
122
+ dataFlows: [
123
+ {
124
+ id: "flow_1",
125
+ type: "api_call",
126
+ confidence: 0.8,
127
+ sourceComponentId: "tp_1",
128
+ targetComponentId: "tp_1",
129
+ },
130
+ ],
131
+ });
132
+ assertMatchesBackendContract(payload);
133
+ });
134
+ });
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const chat_completions_family_1 = require("../../../../src/ai-enrichment/providers/families/chat-completions-family");
4
+ const messages_family_1 = require("../../../../src/ai-enrichment/providers/families/messages-family");
5
+ const resolve_provider_1 = require("../../../../src/ai-enrichment/providers/resolve-provider");
6
+ const provider_enrichment_prompts_1 = require("../../../../src/ai-enrichment/prompts/provider-enrichment-prompts");
7
+ /**
8
+ * DP-P0-CLI-3813: provider families must honor a caller-supplied `systemPrompt`
9
+ * (the DataParade backend owns it on the Platform AI path) and fall back to the
10
+ * bundled prompt when none is given (BYOK path unchanged).
11
+ */
12
+ const OVERRIDE = "SERVER-OWNED SYSTEM PROMPT (override)";
13
+ function jsonOk(body) {
14
+ return {
15
+ ok: true,
16
+ status: 200,
17
+ json: async () => body,
18
+ text: async () => JSON.stringify(body),
19
+ };
20
+ }
21
+ function lastRequestBody(fetchMock) {
22
+ const init = fetchMock.mock.calls[0][1];
23
+ return JSON.parse(String(init.body));
24
+ }
25
+ describe("provider system prompt override (DP-P0-CLI-3813)", () => {
26
+ const originalFetch = global.fetch;
27
+ const userPrompt = JSON.stringify({ agent: "propertyAgent", candidates: [] });
28
+ afterEach(() => {
29
+ global.fetch = originalFetch;
30
+ jest.resetAllMocks();
31
+ });
32
+ describe("OpenAI chat-completions", () => {
33
+ it("uses the override when systemPrompt is set", async () => {
34
+ const fetchMock = jest.fn(async () => jsonOk({ choices: [{ message: { content: '{"proposals":[]}' } }] }));
35
+ global.fetch = fetchMock;
36
+ const provider = new chat_completions_family_1.ChatCompletionsFamilyProvider((0, resolve_provider_1.resolveProviderConfig)("openai", { apiKey: "sk-test" }));
37
+ await provider.infer({
38
+ prompt: userPrompt,
39
+ model: "gpt-4o-mini",
40
+ apiKey: "sk-test",
41
+ systemPrompt: OVERRIDE,
42
+ });
43
+ const body = lastRequestBody(fetchMock);
44
+ const messages = body.messages;
45
+ expect(messages[0]).toEqual({ role: "system", content: OVERRIDE });
46
+ });
47
+ it("falls back to the bundled prompt when systemPrompt is absent (BYOK)", async () => {
48
+ const fetchMock = jest.fn(async () => jsonOk({ choices: [{ message: { content: '{"proposals":[]}' } }] }));
49
+ global.fetch = fetchMock;
50
+ const provider = new chat_completions_family_1.ChatCompletionsFamilyProvider((0, resolve_provider_1.resolveProviderConfig)("openai", { apiKey: "sk-test" }));
51
+ await provider.infer({
52
+ prompt: userPrompt,
53
+ model: "gpt-4o-mini",
54
+ apiKey: "sk-test",
55
+ });
56
+ const body = lastRequestBody(fetchMock);
57
+ const messages = body.messages;
58
+ expect(messages[0]).toEqual({
59
+ role: "system",
60
+ content: provider_enrichment_prompts_1.AI_PROVIDER_SYSTEM_PROMPT,
61
+ });
62
+ });
63
+ });
64
+ describe("Anthropic messages", () => {
65
+ it("uses the override when systemPrompt is set", async () => {
66
+ const fetchMock = jest.fn(async () => jsonOk({ content: [{ type: "text", text: '{"proposals":[]}' }] }));
67
+ global.fetch = fetchMock;
68
+ const provider = new messages_family_1.MessagesFamilyProvider((0, resolve_provider_1.resolveProviderConfig)("anthropic", { apiKey: "sk-ant" }));
69
+ await provider.infer({
70
+ prompt: userPrompt,
71
+ model: "claude-haiku-4-5",
72
+ apiKey: "sk-ant",
73
+ systemPrompt: OVERRIDE,
74
+ });
75
+ const body = lastRequestBody(fetchMock);
76
+ expect(body.system).toBe(OVERRIDE);
77
+ });
78
+ it("falls back to the bundled Anthropic prompt when absent (BYOK)", async () => {
79
+ const fetchMock = jest.fn(async () => jsonOk({ content: [{ type: "text", text: '{"proposals":[]}' }] }));
80
+ global.fetch = fetchMock;
81
+ const provider = new messages_family_1.MessagesFamilyProvider((0, resolve_provider_1.resolveProviderConfig)("anthropic", { apiKey: "sk-ant" }));
82
+ await provider.infer({
83
+ prompt: userPrompt,
84
+ model: "claude-haiku-4-5",
85
+ apiKey: "sk-ant",
86
+ });
87
+ const body = lastRequestBody(fetchMock);
88
+ expect(body.system).toBe((0, provider_enrichment_prompts_1.buildAnthropicEnrichmentSystemPrompt)());
89
+ });
90
+ });
91
+ });
@@ -34,14 +34,33 @@ jest.mock("../../../src/core/pipeline/graph-mapping", () => ({
34
34
  }));
35
35
  jest.mock("../../../src/output/json", () => ({
36
36
  writeDataflowJson: jest.fn(() => { }),
37
+ buildDataflowWrapper: jest.fn(() => ({
38
+ schemaVersion: "1.0",
39
+ graph: { nodes: [], edges: [], viewport: { x: 0, y: 0, zoom: 1 } },
40
+ metadata: {
41
+ componentsCount: 0,
42
+ dataFlowsCount: 0,
43
+ filesScanned: 0,
44
+ scanDurationMs: 0,
45
+ },
46
+ })),
37
47
  }));
38
48
  describe("cli scan command ai flags", () => {
39
49
  let consoleLogSpy;
50
+ let prevSkipAutoUpload;
40
51
  beforeEach(() => {
41
52
  consoleLogSpy = jest.spyOn(console, "log").mockImplementation(() => { });
53
+ prevSkipAutoUpload = process.env.DATAPARADE_SKIP_AUTO_UPLOAD;
54
+ process.env.DATAPARADE_SKIP_AUTO_UPLOAD = "true";
42
55
  });
43
56
  afterEach(() => {
44
57
  consoleLogSpy.mockRestore();
58
+ if (prevSkipAutoUpload === undefined) {
59
+ delete process.env.DATAPARADE_SKIP_AUTO_UPLOAD;
60
+ }
61
+ else {
62
+ process.env.DATAPARADE_SKIP_AUTO_UPLOAD = prevSkipAutoUpload;
63
+ }
45
64
  });
46
65
  it("normalizes --ai-inference-scope third-party-only to third_party_only", async () => {
47
66
  const { run } = require("../../../src/cli");
@@ -12,7 +12,16 @@ describe("cli scan command - DP-P0-CLI-402 integration", () => {
12
12
  it("writes a valid dataflow.json wrapper to the output path", async () => {
13
13
  const fixturesRoot = path_1.default.join(__dirname, "..", "..", "fixtures", "typescript-basic");
14
14
  const outputPath = path_1.default.join(os_1.default.tmpdir(), `dataparade-scan-${Date.now()}.json`);
15
- await (0, cli_1.run)(["node", "cli", "scan", fixturesRoot, "--output", outputPath]);
15
+ await (0, cli_1.run)([
16
+ "node",
17
+ "cli",
18
+ "scan",
19
+ fixturesRoot,
20
+ "--output",
21
+ outputPath,
22
+ "--no-ai-inference",
23
+ "--skip-auto-upload",
24
+ ]);
16
25
  const contents = fs_1.default.readFileSync(outputPath, "utf8");
17
26
  const parsed = JSON.parse(contents);
18
27
  const validation = (0, dataflow_wrapper_schema_1.validateDataflowJson)(parsed);
@@ -21,12 +21,19 @@ function nodeSourcePaths(node) {
21
21
  }
22
22
  describe("cli scan command - monorepo app plus terraform", () => {
23
23
  it("keeps app API hub, reduces TF to provider and module shells, and lays out app left of TF hub", async () => {
24
- const prevAiInference = process.env.DATAPARADE_AI_INFERENCE;
25
- process.env.DATAPARADE_AI_INFERENCE = "false";
26
24
  const fixturesRoot = path_1.default.join(__dirname, "..", "..", "fixtures", "monorepo-app-plus-terraform");
27
25
  const outputPath = path_1.default.join(os_1.default.tmpdir(), `dataparade-scan-e2e-monorepo-app-plus-terraform-${Date.now()}.json`);
26
+ await (0, cli_1.run)([
27
+ "node",
28
+ "cli",
29
+ "scan",
30
+ fixturesRoot,
31
+ "--output",
32
+ outputPath,
33
+ "--no-ai-inference",
34
+ "--skip-auto-upload",
35
+ ]);
28
36
  try {
29
- await (0, cli_1.run)(["node", "cli", "scan", fixturesRoot, "--output", outputPath]);
30
37
  const contents = fs_1.default.readFileSync(outputPath, "utf8");
31
38
  const parsed = JSON.parse(contents);
32
39
  const validation = (0, dataflow_wrapper_schema_1.validateDataflowJson)(parsed);
@@ -96,11 +103,8 @@ describe("cli scan command - monorepo app plus terraform", () => {
96
103
  fs_1.default.unlinkSync(outputPath);
97
104
  }
98
105
  finally {
99
- if (prevAiInference === undefined) {
100
- delete process.env.DATAPARADE_AI_INFERENCE;
101
- }
102
- else {
103
- process.env.DATAPARADE_AI_INFERENCE = prevAiInference;
106
+ if (fs_1.default.existsSync(outputPath)) {
107
+ fs_1.default.unlinkSync(outputPath);
104
108
  }
105
109
  }
106
110
  }, 20000);
@@ -12,7 +12,16 @@ describe("cli scan command - monorepo sections", () => {
12
12
  it("tags nodes with section_id and avoids cross-section edges", async () => {
13
13
  const fixturesRoot = path_1.default.join(__dirname, "..", "..", "fixtures", "monorepo-front-back-sections");
14
14
  const outputPath = path_1.default.join(os_1.default.tmpdir(), `dataparade-scan-e2e-monorepo-front-back-sections-${Date.now()}.json`);
15
- await (0, cli_1.run)(["node", "cli", "scan", fixturesRoot, "--output", outputPath]);
15
+ await (0, cli_1.run)([
16
+ "node",
17
+ "cli",
18
+ "scan",
19
+ fixturesRoot,
20
+ "--output",
21
+ outputPath,
22
+ "--no-ai-inference",
23
+ "--skip-auto-upload",
24
+ ]);
16
25
  const contents = fs_1.default.readFileSync(outputPath, "utf8");
17
26
  const parsed = JSON.parse(contents);
18
27
  const validation = (0, dataflow_wrapper_schema_1.validateDataflowJson)(parsed);
@@ -12,7 +12,16 @@ describe("cli scan command - DP-P0-CLI-705 e2e Python basic", () => {
12
12
  it("scans python-basic fixture and produces valid dataflow.json with route, database, third-party, and flows", async () => {
13
13
  const fixturesRoot = path_1.default.join(__dirname, "..", "..", "fixtures", "python-basic");
14
14
  const outputPath = path_1.default.join(os_1.default.tmpdir(), `dataparade-scan-e2e-python-basic-${Date.now()}.json`);
15
- await (0, cli_1.run)(["node", "cli", "scan", fixturesRoot, "--output", outputPath]);
15
+ await (0, cli_1.run)([
16
+ "node",
17
+ "cli",
18
+ "scan",
19
+ fixturesRoot,
20
+ "--output",
21
+ outputPath,
22
+ "--no-ai-inference",
23
+ "--skip-auto-upload",
24
+ ]);
16
25
  const contents = fs_1.default.readFileSync(outputPath, "utf8");
17
26
  const parsed = JSON.parse(contents);
18
27
  const validation = (0, dataflow_wrapper_schema_1.validateDataflowJson)(parsed);