@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.
- package/README.md +3 -2
- package/dist/package.json +2 -2
- package/dist/src/ai-enrichment/providers/families/chat-completions-family.js +4 -1
- package/dist/src/ai-enrichment/providers/families/generate-content-family.js +3 -1
- package/dist/src/ai-enrichment/providers/families/messages-family.js +1 -1
- package/dist/src/ai-enrichment/providers/families/ollama-generate-family.js +1 -1
- package/dist/src/ai-enrichment/providers/types.d.ts +6 -0
- package/dist/src/cli.js +85 -41
- package/dist/src/config/env.js +7 -2
- package/dist/src/config/load-cli-env.d.ts +2 -0
- package/dist/src/config/load-cli-env.js +11 -2
- package/dist/src/config/redact.js +3 -0
- package/dist/src/config/validate-scan-ai.js +13 -7
- package/dist/src/core/pipeline/ai-orchestrator-options.js +8 -4
- package/dist/src/core/pipeline/orchestrator.js +1 -1
- package/dist/src/core/schema/scan-config.schema.d.ts +1 -0
- package/dist/src/core/schema/scan-config.schema.js +1 -0
- package/dist/src/core/types/config.d.ts +5 -0
- package/dist/src/platform-api/anonymous-ai-session-client.d.ts +14 -0
- package/dist/src/platform-api/anonymous-ai-session-client.js +40 -0
- package/dist/src/platform-api/read-cli-api-error-message.d.ts +18 -0
- package/dist/src/platform-api/read-cli-api-error-message.js +31 -0
- package/dist/src/platform-api/scan-quota-client.d.ts +5 -0
- package/dist/src/platform-api/scan-quota-client.js +24 -14
- package/dist/src/platform-api/upload-client.d.ts +10 -3
- package/dist/src/platform-api/upload-client.js +35 -16
- package/dist/src/platform-api/upload.types.d.ts +10 -5
- package/dist/src/upload/build-preview-url.d.ts +1 -0
- package/dist/src/upload/build-preview-url.js +6 -0
- package/dist/src/upload/run-upload.js +20 -4
- package/dist/src/upload/types.d.ts +2 -2
- package/dist/tests/cli/cli.spec.js +17 -3
- package/dist/tests/unit/ai-enrichment/provider-prompt-backend-contract.spec.d.ts +1 -0
- package/dist/tests/unit/ai-enrichment/provider-prompt-backend-contract.spec.js +134 -0
- package/dist/tests/unit/ai-enrichment/providers/system-prompt-override.spec.d.ts +1 -0
- package/dist/tests/unit/ai-enrichment/providers/system-prompt-override.spec.js +91 -0
- package/dist/tests/unit/cli/scan-command-ai-flags.spec.js +19 -0
- package/dist/tests/unit/cli/scan-command-diagram-graph.spec.js +10 -1
- package/dist/tests/unit/cli/scan-command-e2e-monorepo-app-plus-terraform.spec.js +12 -8
- package/dist/tests/unit/cli/scan-command-e2e-monorepo-front-back-sections.spec.js +10 -1
- package/dist/tests/unit/cli/scan-command-e2e-python-basic.spec.js +10 -1
- package/dist/tests/unit/cli/scan-command-e2e-terraform-basic.spec.js +12 -8
- package/dist/tests/unit/cli/scan-command-e2e-ts-sample.spec.js +10 -1
- package/dist/tests/unit/cli/scan-command-e2e-ts-sentry-route-env.spec.js +1 -1
- package/dist/tests/unit/cli/scan-command-exit-code.spec.js +23 -0
- package/dist/tests/unit/cli/scan-quota-flow.spec.js +44 -1
- package/dist/tests/unit/config/load-cli-env.spec.d.ts +1 -0
- package/dist/tests/unit/config/load-cli-env.spec.js +52 -0
- package/dist/tests/unit/config/redact.spec.js +1 -1
- package/dist/tests/unit/config/resolve-config.spec.js +1 -1
- package/dist/tests/unit/config/validate-scan-ai.spec.js +25 -0
- package/dist/tests/unit/core/ai-orchestrator-options-anon.spec.d.ts +1 -0
- package/dist/tests/unit/core/ai-orchestrator-options-anon.spec.js +29 -0
- package/dist/tests/unit/core/dependency-manifest-performance-budgets.spec.js +1 -1
- package/dist/tests/unit/core/graph-mapping.spec.js +1 -1
- package/dist/tests/unit/core/invariant-tests-dedupe-determinism.spec.js +1 -1
- package/dist/tests/unit/core/no-source-code-leakage.spec.js +1 -1
- package/dist/tests/unit/core/orchestrator.spec.js +12 -12
- package/dist/tests/unit/core/output-json.spec.js +2 -2
- package/dist/tests/unit/core/parsing-error-handling-nonfatal.spec.js +1 -1
- package/dist/tests/unit/core/scan-result-language-stats.spec.js +1 -1
- package/dist/tests/unit/core/structural-scan.spec.js +1 -1
- package/dist/tests/unit/core/terraform-json-overlay.spec.js +1 -1
- package/dist/tests/unit/platform-api/anonymous-ai-session-client.spec.d.ts +1 -0
- package/dist/tests/unit/platform-api/anonymous-ai-session-client.spec.js +35 -0
- package/dist/tests/unit/platform-api/read-cli-api-error-message.spec.d.ts +1 -0
- package/dist/tests/unit/platform-api/read-cli-api-error-message.spec.js +38 -0
- package/dist/tests/unit/upload/build-preview-url.spec.js +16 -0
- package/dist/tests/unit/upload/run-upload.spec.d.ts +1 -0
- package/dist/tests/unit/upload/run-upload.spec.js +43 -0
- 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
|
-
|
|
44
|
+
const fallback = `Scan preflight failed (${res.status})`;
|
|
35
45
|
let code;
|
|
46
|
+
let message = fallback;
|
|
36
47
|
try {
|
|
37
|
-
const body =
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
78
|
+
const fallback = `Scan complete report failed (${res.status})`;
|
|
79
|
+
let message = fallback;
|
|
67
80
|
try {
|
|
68
|
-
const body =
|
|
69
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
|
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,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
|
-
|
|
9
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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", [
|
|
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", [
|
|
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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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)([
|
|
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 (
|
|
100
|
-
|
|
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)([
|
|
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)([
|
|
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);
|