@dataparade/cli 0.0.7 → 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/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 +20 -5
- 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/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-diagram-graph.spec.js +10 -1
- package/dist/tests/unit/cli/scan-command-exit-code.spec.js +2 -0
- package/dist/tests/unit/cli/scan-quota-flow.spec.js +43 -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/package.json +2 -2
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dataparade/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "dataPARADE CLI — scan codebases for privacy and security data flows",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"license": "GPL-3.0-or-later",
|
|
10
10
|
"author": "DataParade",
|
|
11
|
-
"homepage": "https://
|
|
11
|
+
"homepage": "https://dataparade.io",
|
|
12
12
|
"repository": {
|
|
13
13
|
"type": "git",
|
|
14
14
|
"url": "git+https://github.com/DataParade-io/dataparade-cli.git"
|
|
@@ -103,7 +103,10 @@ class ChatCompletionsFamilyProvider {
|
|
|
103
103
|
const body = {
|
|
104
104
|
model,
|
|
105
105
|
messages: [
|
|
106
|
-
{
|
|
106
|
+
{
|
|
107
|
+
role: "system",
|
|
108
|
+
content: request.systemPrompt ?? provider_enrichment_prompts_1.AI_PROVIDER_SYSTEM_PROMPT,
|
|
109
|
+
},
|
|
107
110
|
{ role: "user", content: request.prompt },
|
|
108
111
|
],
|
|
109
112
|
temperature: request.temperature ?? 0.1,
|
|
@@ -34,7 +34,9 @@ class GenerateContentFamilyProvider {
|
|
|
34
34
|
"content-type": "application/json",
|
|
35
35
|
},
|
|
36
36
|
body: JSON.stringify({
|
|
37
|
-
systemInstruction: {
|
|
37
|
+
systemInstruction: {
|
|
38
|
+
parts: [{ text: request.systemPrompt ?? provider_enrichment_prompts_1.AI_PROVIDER_SYSTEM_PROMPT }],
|
|
39
|
+
},
|
|
38
40
|
contents: [
|
|
39
41
|
{
|
|
40
42
|
role: "user",
|
|
@@ -33,7 +33,7 @@ class MessagesFamilyProvider {
|
|
|
33
33
|
model,
|
|
34
34
|
max_tokens: maxTokens,
|
|
35
35
|
temperature: request.temperature ?? 0.1,
|
|
36
|
-
system: (0, provider_enrichment_prompts_1.buildAnthropicEnrichmentSystemPrompt)(),
|
|
36
|
+
system: request.systemPrompt ?? (0, provider_enrichment_prompts_1.buildAnthropicEnrichmentSystemPrompt)(),
|
|
37
37
|
messages: [{ role: "user", content: request.prompt }],
|
|
38
38
|
}),
|
|
39
39
|
});
|
|
@@ -23,7 +23,7 @@ class OllamaGenerateFamilyProvider {
|
|
|
23
23
|
},
|
|
24
24
|
body: JSON.stringify({
|
|
25
25
|
model,
|
|
26
|
-
prompt: `${provider_enrichment_prompts_1.AI_PROVIDER_SYSTEM_PROMPT}\n\nUser payload:\n${request.prompt}`,
|
|
26
|
+
prompt: `${request.systemPrompt ?? provider_enrichment_prompts_1.AI_PROVIDER_SYSTEM_PROMPT}\n\nUser payload:\n${request.prompt}`,
|
|
27
27
|
stream: false,
|
|
28
28
|
options: {
|
|
29
29
|
temperature: request.temperature ?? 0.1,
|
|
@@ -6,6 +6,12 @@ export interface AiProviderRequest {
|
|
|
6
6
|
endpoint?: string;
|
|
7
7
|
temperature?: number;
|
|
8
8
|
maxTokens?: number;
|
|
9
|
+
/**
|
|
10
|
+
* Overrides the bundled enrichment system prompt (DP-P0-CLI-3813).
|
|
11
|
+
* Set by the DataParade backend on the Platform AI path so the server owns
|
|
12
|
+
* the system role. Unset for BYOK, which keeps the CLI-bundled prompt.
|
|
13
|
+
*/
|
|
14
|
+
systemPrompt?: string;
|
|
9
15
|
}
|
|
10
16
|
export interface AiProviderUsage {
|
|
11
17
|
inputTokens: number;
|
package/dist/src/cli.js
CHANGED
|
@@ -155,6 +155,7 @@ function createProgram() {
|
|
|
155
155
|
let sentryScanRoot;
|
|
156
156
|
let sentryAiMode;
|
|
157
157
|
let sentryAiProvider;
|
|
158
|
+
const isInteractive = process.stdout.isTTY;
|
|
158
159
|
const workspaceApiKey = options.workspaceApiKey?.trim() ||
|
|
159
160
|
options.apiKey?.trim() ||
|
|
160
161
|
(0, scan_env_1.resolveWorkspaceApiKey)(process.env);
|
|
@@ -182,7 +183,6 @@ function createProgram() {
|
|
|
182
183
|
process.exitCode = 2;
|
|
183
184
|
return;
|
|
184
185
|
}
|
|
185
|
-
const isInteractive = process.stdout.isTTY;
|
|
186
186
|
const [{ scan, createDefaultScanConfiguration }, { buildDiagramGraphFromScanResult }] = await Promise.all([
|
|
187
187
|
Promise.resolve().then(() => __importStar(require("./core/pipeline/orchestrator"))),
|
|
188
188
|
Promise.resolve().then(() => __importStar(require("./core/pipeline/graph-mapping"))),
|
|
@@ -482,26 +482,37 @@ function createProgram() {
|
|
|
482
482
|
failureMessage: exitFailed ? "CLI scan did not complete successfully" : undefined,
|
|
483
483
|
});
|
|
484
484
|
quotaCompletionReported = true;
|
|
485
|
+
if (isInteractive) {
|
|
486
|
+
// eslint-disable-next-line no-console
|
|
487
|
+
console.log("[scan] Scan finished — you can start a new scan now.");
|
|
488
|
+
}
|
|
485
489
|
}
|
|
486
490
|
}
|
|
487
491
|
catch (err) {
|
|
488
492
|
const message = err instanceof Error ? err.message : "Unknown error during scan.";
|
|
489
493
|
fallbackFailureMessage = message;
|
|
490
|
-
const { CliScanQuotaExceededError } = await Promise.resolve().then(() => __importStar(require("./platform-api/scan-quota-client")));
|
|
494
|
+
const { CliScanQuotaExceededError, CliScanAlreadyRunningError } = await Promise.resolve().then(() => __importStar(require("./platform-api/scan-quota-client")));
|
|
491
495
|
const quotaBlocked = err instanceof CliScanQuotaExceededError;
|
|
496
|
+
const scanInProgress = err instanceof CliScanAlreadyRunningError;
|
|
492
497
|
await (0, scan_sentry_1.reportScanCliError)({
|
|
493
498
|
error: err,
|
|
494
499
|
scanRoot: sentryScanRoot,
|
|
495
500
|
jobId: cliQuotaJobId,
|
|
496
501
|
aiMode: sentryAiMode,
|
|
497
502
|
aiProvider: sentryAiProvider,
|
|
498
|
-
failurePhase: quotaBlocked ? "preflight" : "scan_command",
|
|
499
|
-
failureCode: quotaBlocked
|
|
503
|
+
failurePhase: quotaBlocked || scanInProgress ? "preflight" : "scan_command",
|
|
504
|
+
failureCode: quotaBlocked
|
|
505
|
+
? "scan_quota_exceeded"
|
|
506
|
+
: scanInProgress
|
|
507
|
+
? "scan_already_running"
|
|
508
|
+
: "scan_exception",
|
|
500
509
|
});
|
|
501
510
|
// eslint-disable-next-line no-console
|
|
502
511
|
console.error(quotaBlocked
|
|
503
512
|
? `[scan] workspace quota: ${message}`
|
|
504
|
-
:
|
|
513
|
+
: scanInProgress
|
|
514
|
+
? `[scan] scan in progress: ${message}`
|
|
515
|
+
: `Scan failed: ${message}`);
|
|
505
516
|
process.exitCode = 1;
|
|
506
517
|
}
|
|
507
518
|
finally {
|
|
@@ -516,6 +527,10 @@ function createProgram() {
|
|
|
516
527
|
failureMessage: fallbackFailureMessage,
|
|
517
528
|
});
|
|
518
529
|
quotaCompletionReported = true;
|
|
530
|
+
if (isInteractive) {
|
|
531
|
+
// eslint-disable-next-line no-console
|
|
532
|
+
console.log("[scan] Scan finished — you can start a new scan now.");
|
|
533
|
+
}
|
|
519
534
|
}
|
|
520
535
|
catch {
|
|
521
536
|
// Quota report failure must not mask the original scan error.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
type NestedApiError = {
|
|
2
|
+
code?: string;
|
|
3
|
+
message?: string;
|
|
4
|
+
};
|
|
5
|
+
export type CliApiErrorBody = {
|
|
6
|
+
code?: string;
|
|
7
|
+
message?: string | string[] | NestedApiError;
|
|
8
|
+
statusCode?: number;
|
|
9
|
+
};
|
|
10
|
+
export declare function parseCliApiErrorBody(body: unknown): {
|
|
11
|
+
code?: string;
|
|
12
|
+
message?: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function readCliApiErrorMessage(body: unknown, fallback: string): {
|
|
15
|
+
code?: string;
|
|
16
|
+
message: string;
|
|
17
|
+
};
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseCliApiErrorBody = parseCliApiErrorBody;
|
|
4
|
+
exports.readCliApiErrorMessage = readCliApiErrorMessage;
|
|
5
|
+
function parseCliApiErrorBody(body) {
|
|
6
|
+
if (!body || typeof body !== "object") {
|
|
7
|
+
return {};
|
|
8
|
+
}
|
|
9
|
+
const parsed = body;
|
|
10
|
+
if (typeof parsed.message === "string") {
|
|
11
|
+
return { code: parsed.code, message: parsed.message };
|
|
12
|
+
}
|
|
13
|
+
if (Array.isArray(parsed.message)) {
|
|
14
|
+
return { code: parsed.code, message: parsed.message.join(", ") };
|
|
15
|
+
}
|
|
16
|
+
if (parsed.message && typeof parsed.message === "object") {
|
|
17
|
+
const nested = parsed.message;
|
|
18
|
+
return {
|
|
19
|
+
code: nested.code ?? parsed.code,
|
|
20
|
+
message: typeof nested.message === "string" ? nested.message : undefined,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return { code: parsed.code };
|
|
24
|
+
}
|
|
25
|
+
function readCliApiErrorMessage(body, fallback) {
|
|
26
|
+
const parsed = parseCliApiErrorBody(body);
|
|
27
|
+
return {
|
|
28
|
+
code: parsed.code,
|
|
29
|
+
message: parsed.message ?? fallback,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -3,6 +3,11 @@ export declare class CliScanQuotaExceededError extends Error {
|
|
|
3
3
|
readonly code: "scan_quota_exceeded";
|
|
4
4
|
constructor(message: string);
|
|
5
5
|
}
|
|
6
|
+
/** Thrown when preflight is rejected with HTTP 409 scan_already_running. */
|
|
7
|
+
export declare class CliScanAlreadyRunningError extends Error {
|
|
8
|
+
readonly code: "scan_already_running";
|
|
9
|
+
constructor(message: string);
|
|
10
|
+
}
|
|
6
11
|
export type CliScanPreflightResponse = {
|
|
7
12
|
allowed: boolean;
|
|
8
13
|
jobId: string;
|
|
@@ -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
|
|
@@ -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
|
+
});
|
|
@@ -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);
|
|
@@ -107,6 +107,7 @@ describe("cli scan command - exit codes", () => {
|
|
|
107
107
|
fixturesRoot,
|
|
108
108
|
"--output",
|
|
109
109
|
tmpOutputPath(),
|
|
110
|
+
"--no-ai-inference",
|
|
110
111
|
]);
|
|
111
112
|
expect(process.exitCode).toBe(1);
|
|
112
113
|
expect(outputJson.writeDataflowJson).toHaveBeenCalled();
|
|
@@ -144,6 +145,7 @@ describe("cli scan command - exit codes", () => {
|
|
|
144
145
|
fixturesRoot,
|
|
145
146
|
"--output",
|
|
146
147
|
outputPath,
|
|
148
|
+
"--no-ai-inference",
|
|
147
149
|
]);
|
|
148
150
|
expect(process.exitCode).toBe(1);
|
|
149
151
|
expect(outputJson.writeDataflowJson).not.toHaveBeenCalled();
|
|
@@ -49,6 +49,20 @@ describe("scan quota flow", () => {
|
|
|
49
49
|
expect(errorSpy).toHaveBeenCalledWith("[scan] workspace quota: No scan slots remaining in this workspace.");
|
|
50
50
|
errorSpy.mockRestore();
|
|
51
51
|
});
|
|
52
|
+
it("prints scan in progress message when preflight is blocked by running job", async () => {
|
|
53
|
+
process.env.DATAPARADE_WORKSPACE_API_KEY = "dp_live_test";
|
|
54
|
+
const errorSpy = jest.spyOn(console, "error").mockImplementation(() => { });
|
|
55
|
+
fetchMock.mockResolvedValueOnce(jsonResponse({
|
|
56
|
+
statusCode: 409,
|
|
57
|
+
message: {
|
|
58
|
+
code: "scan_already_running",
|
|
59
|
+
message: "Scan in progress — start a new scan after the current one completes.",
|
|
60
|
+
},
|
|
61
|
+
}, 409));
|
|
62
|
+
await (0, cli_1.run)(["node", "cli", "scan", tempRoot, "--ai-inference"]);
|
|
63
|
+
expect(errorSpy).toHaveBeenCalledWith("[scan] scan in progress: Scan in progress — start a new scan after the current one completes.");
|
|
64
|
+
errorSpy.mockRestore();
|
|
65
|
+
});
|
|
52
66
|
it("reports failed completion when preflight succeeds but config validation fails", async () => {
|
|
53
67
|
process.env.DATAPARADE_WORKSPACE_API_KEY = "dp_live_test";
|
|
54
68
|
fetchMock
|
|
@@ -75,4 +89,33 @@ describe("scan quota flow", () => {
|
|
|
75
89
|
expect(String(fetchMock.mock.calls[1][0])).toContain("/api/scans/cli/job-123/complete");
|
|
76
90
|
expect(JSON.parse(String(fetchMock.mock.calls[1][1]?.body))).toEqual(expect.objectContaining({ status: "failed", failureCode: "scan_failed" }));
|
|
77
91
|
});
|
|
92
|
+
it("prints ready-for-new-scan message after platform scan completes", async () => {
|
|
93
|
+
process.env.DATAPARADE_WORKSPACE_API_KEY = "dp_live_test";
|
|
94
|
+
process.env.DATAPARADE_SKIP_AUTO_UPLOAD = "true";
|
|
95
|
+
const logSpy = jest.spyOn(console, "log").mockImplementation(() => { });
|
|
96
|
+
const isTty = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
|
|
97
|
+
Object.defineProperty(process.stdout, "isTTY", {
|
|
98
|
+
value: true,
|
|
99
|
+
configurable: true,
|
|
100
|
+
});
|
|
101
|
+
fetchMock
|
|
102
|
+
.mockResolvedValueOnce(jsonResponse({
|
|
103
|
+
allowed: true,
|
|
104
|
+
jobId: "job-456",
|
|
105
|
+
scansRemaining: 2,
|
|
106
|
+
aiTokensRemaining: 1000,
|
|
107
|
+
suggestedAiBudgetTokens: 100,
|
|
108
|
+
aiDelivery: "platform_proxy",
|
|
109
|
+
}))
|
|
110
|
+
.mockResolvedValueOnce(jsonResponse({ ok: true }));
|
|
111
|
+
await (0, cli_1.run)(["node", "cli", "scan", tempRoot, "--ai-inference"]);
|
|
112
|
+
expect(logSpy).toHaveBeenCalledWith("[scan] Scan finished — you can start a new scan now.");
|
|
113
|
+
if (isTty) {
|
|
114
|
+
Object.defineProperty(process.stdout, "isTTY", isTty);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
delete process.stdout.isTTY;
|
|
118
|
+
}
|
|
119
|
+
logSpy.mockRestore();
|
|
120
|
+
});
|
|
78
121
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const read_cli_api_error_message_1 = require("../../../src/platform-api/read-cli-api-error-message");
|
|
4
|
+
describe("readCliApiErrorMessage", () => {
|
|
5
|
+
it("reads flat quota error body", () => {
|
|
6
|
+
expect((0, read_cli_api_error_message_1.readCliApiErrorMessage)({
|
|
7
|
+
code: "scan_quota_exceeded",
|
|
8
|
+
message: "No scan slots remaining in this workspace.",
|
|
9
|
+
}, "fallback")).toEqual({
|
|
10
|
+
code: "scan_quota_exceeded",
|
|
11
|
+
message: "No scan slots remaining in this workspace.",
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
it("reads nested Nest conflict error body", () => {
|
|
15
|
+
expect((0, read_cli_api_error_message_1.readCliApiErrorMessage)({
|
|
16
|
+
statusCode: 409,
|
|
17
|
+
message: {
|
|
18
|
+
code: "scan_already_running",
|
|
19
|
+
message: "Scan in progress — start a new scan after the current one completes.",
|
|
20
|
+
},
|
|
21
|
+
}, "fallback")).toEqual({
|
|
22
|
+
code: "scan_already_running",
|
|
23
|
+
message: "Scan in progress — start a new scan after the current one completes.",
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
it("joins validation message arrays", () => {
|
|
27
|
+
expect((0, read_cli_api_error_message_1.parseCliApiErrorBody)({
|
|
28
|
+
message: ["field is required", "field must be a string"],
|
|
29
|
+
})).toEqual({
|
|
30
|
+
message: "field is required, field must be a string",
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
it("falls back when message is missing", () => {
|
|
34
|
+
expect((0, read_cli_api_error_message_1.readCliApiErrorMessage)({}, "Scan preflight failed (500)")).toEqual({
|
|
35
|
+
message: "Scan preflight failed (500)",
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dataparade/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "dataPARADE CLI — scan codebases for privacy and security data flows",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"license": "GPL-3.0-or-later",
|
|
10
10
|
"author": "DataParade",
|
|
11
|
-
"homepage": "https://
|
|
11
|
+
"homepage": "https://dataparade.io",
|
|
12
12
|
"repository": {
|
|
13
13
|
"type": "git",
|
|
14
14
|
"url": "git+https://github.com/DataParade-io/dataparade-cli.git"
|