@euconform/core 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +18 -0
- package/assets/datasets/crows-pairs-de.json +613 -0
- package/assets/datasets/crows-pairs-en.json +9055 -0
- package/dist/datasets/crows-pairs.d.ts +7 -0
- package/dist/datasets/crows-pairs.d.ts.map +1 -0
- package/dist/datasets/crows-pairs.js +41 -0
- package/dist/evidence/bundle.d.ts +23 -0
- package/dist/evidence/bundle.d.ts.map +1 -0
- package/dist/evidence/bundle.js +44 -0
- package/dist/evidence/compliance-evaluation.d.ts +12 -0
- package/dist/evidence/compliance-evaluation.d.ts.map +1 -0
- package/dist/evidence/compliance-evaluation.js +78 -0
- package/dist/evidence/hash.d.ts +2 -0
- package/dist/evidence/hash.d.ts.map +1 -0
- package/dist/evidence/hash.js +4 -0
- package/dist/evidence/index.d.ts +13 -0
- package/dist/evidence/index.d.ts.map +1 -0
- package/dist/evidence/index.js +11 -0
- package/dist/evidence/markdown.d.ts +9 -0
- package/dist/evidence/markdown.d.ts.map +1 -0
- package/dist/evidence/markdown.js +169 -0
- package/dist/evidence/output.d.ts +13 -0
- package/dist/evidence/output.d.ts.map +1 -0
- package/dist/evidence/output.js +547 -0
- package/dist/evidence/types.d.ts +230 -0
- package/dist/evidence/types.d.ts.map +1 -0
- package/dist/evidence/types.js +7 -0
- package/dist/evidence/validate.d.ts +8 -0
- package/dist/evidence/validate.d.ts.map +1 -0
- package/dist/evidence/validate.js +192 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/inference/index.d.ts +1 -1
- package/dist/inference/index.d.ts.map +1 -1
- package/dist/inference/index.js +1 -1
- package/dist/inference/ollama-client.d.ts +11 -3
- package/dist/inference/ollama-client.d.ts.map +1 -1
- package/dist/inference/ollama-client.js +84 -84
- package/dist/scanner/aggregator.d.ts +7 -0
- package/dist/scanner/aggregator.d.ts.map +1 -0
- package/dist/scanner/aggregator.js +200 -0
- package/dist/scanner/detectors/ai-framework-detector.d.ts +3 -0
- package/dist/scanner/detectors/ai-framework-detector.d.ts.map +1 -0
- package/dist/scanner/detectors/ai-framework-detector.js +74 -0
- package/dist/scanner/detectors/ai-provider-detector.d.ts +3 -0
- package/dist/scanner/detectors/ai-provider-detector.d.ts.map +1 -0
- package/dist/scanner/detectors/ai-provider-detector.js +100 -0
- package/dist/scanner/detectors/compliance-detector.d.ts +3 -0
- package/dist/scanner/detectors/compliance-detector.d.ts.map +1 -0
- package/dist/scanner/detectors/compliance-detector.js +201 -0
- package/dist/scanner/detectors/config-detector.d.ts +3 -0
- package/dist/scanner/detectors/config-detector.d.ts.map +1 -0
- package/dist/scanner/detectors/config-detector.js +248 -0
- package/dist/scanner/detectors/framework-detector.d.ts +3 -0
- package/dist/scanner/detectors/framework-detector.d.ts.map +1 -0
- package/dist/scanner/detectors/framework-detector.js +141 -0
- package/dist/scanner/detectors/index.d.ts +8 -0
- package/dist/scanner/detectors/index.d.ts.map +1 -0
- package/dist/scanner/detectors/index.js +37 -0
- package/dist/scanner/detectors/local-inference-detector.d.ts +3 -0
- package/dist/scanner/detectors/local-inference-detector.d.ts.map +1 -0
- package/dist/scanner/detectors/local-inference-detector.js +93 -0
- package/dist/scanner/detectors/markdown-detector.d.ts +3 -0
- package/dist/scanner/detectors/markdown-detector.d.ts.map +1 -0
- package/dist/scanner/detectors/markdown-detector.js +139 -0
- package/dist/scanner/detectors/package-json-detector.d.ts +3 -0
- package/dist/scanner/detectors/package-json-detector.d.ts.map +1 -0
- package/dist/scanner/detectors/package-json-detector.js +166 -0
- package/dist/scanner/detectors/rag-detector.d.ts +3 -0
- package/dist/scanner/detectors/rag-detector.d.ts.map +1 -0
- package/dist/scanner/detectors/rag-detector.js +132 -0
- package/dist/scanner/detectors/shared.d.ts +18 -0
- package/dist/scanner/detectors/shared.d.ts.map +1 -0
- package/dist/scanner/detectors/shared.js +36 -0
- package/dist/scanner/discovery.d.ts +14 -0
- package/dist/scanner/discovery.d.ts.map +1 -0
- package/dist/scanner/discovery.js +235 -0
- package/dist/scanner/index.d.ts +9 -0
- package/dist/scanner/index.d.ts.map +1 -0
- package/dist/scanner/index.js +8 -0
- package/package.json +8 -3
- package/src/datasets/crows-pairs.ts +64 -0
- package/src/evidence/bundle.ts +65 -0
- package/src/evidence/compliance-evaluation.ts +114 -0
- package/src/evidence/hash.ts +5 -0
- package/src/evidence/index.ts +51 -0
- package/src/evidence/markdown.ts +226 -0
- package/src/evidence/output.ts +717 -0
- package/src/evidence/types.ts +329 -0
- package/src/evidence/validate.ts +300 -0
- package/src/index.ts +1 -0
- package/src/inference/index.ts +6 -1
- package/src/inference/ollama-client.ts +111 -104
- package/src/scanner/aggregator.ts +258 -0
- package/src/scanner/detectors/ai-framework-detector.ts +94 -0
- package/src/scanner/detectors/ai-provider-detector.ts +135 -0
- package/src/scanner/detectors/compliance-detector.ts +232 -0
- package/src/scanner/detectors/config-detector.ts +316 -0
- package/src/scanner/detectors/framework-detector.ts +163 -0
- package/src/scanner/detectors/index.ts +40 -0
- package/src/scanner/detectors/local-inference-detector.ts +124 -0
- package/src/scanner/detectors/markdown-detector.ts +176 -0
- package/src/scanner/detectors/package-json-detector.ts +202 -0
- package/src/scanner/detectors/rag-detector.ts +162 -0
- package/src/scanner/detectors/shared.ts +45 -0
- package/src/scanner/discovery.ts +290 -0
- package/src/scanner/index.ts +9 -0
- package/tests/evidence/bundle.test.ts +128 -0
- package/tests/evidence/hash.test.ts +25 -0
- package/tests/evidence/markdown.test.ts +92 -0
- package/tests/evidence/output.test.ts +203 -0
- package/tests/evidence/spec-schemas.test.ts +316 -0
- package/tests/fixtures/compliance-good/README.md +14 -0
- package/tests/fixtures/compliance-good/lib/ai-service.ts +17 -0
- package/tests/fixtures/compliance-good/lib/review-flow.ts +11 -0
- package/tests/fixtures/compliance-good/package.json +10 -0
- package/tests/fixtures/compliance-good/tests/bias-check.test.ts +13 -0
- package/tests/fixtures/nextjs-openai/README.md +5 -0
- package/tests/fixtures/nextjs-openai/app/page.tsx +11 -0
- package/tests/fixtures/nextjs-openai/package.json +9 -0
- package/tests/fixtures/ollama-rag/.env.example +2 -0
- package/tests/fixtures/ollama-rag/docker-compose.yml +9 -0
- package/tests/fixtures/ollama-rag/package.json +9 -0
- package/tests/fixtures/ollama-rag/server.ts +21 -0
- package/tests/fixtures/plain-webapp/README.md +3 -0
- package/tests/fixtures/plain-webapp/index.js +5 -0
- package/tests/fixtures/plain-webapp/package.json +8 -0
- package/tests/gpai-classifier.test.ts +4 -4
- package/tests/gpai-timeline.test.ts +2 -2
- package/tests/scanner/aggregator.test.ts +122 -0
- package/tests/scanner/detectors.test.ts +274 -0
- package/tests/scanner/discovery.test.ts +87 -0
- package/vitest.config.ts +1 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import OpenAI from "openai";
|
|
2
|
+
|
|
3
|
+
const client = new OpenAI();
|
|
4
|
+
|
|
5
|
+
export default async function Page() {
|
|
6
|
+
const response = await client.chat.completions.create({
|
|
7
|
+
model: "gpt-4",
|
|
8
|
+
messages: [{ role: "user", content: "Hello" }],
|
|
9
|
+
});
|
|
10
|
+
return <div>AI Response: {response.choices[0]?.message?.content}</div>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ChromaClient } from "chromadb";
|
|
2
|
+
import express from "express";
|
|
3
|
+
import { Ollama } from "ollama";
|
|
4
|
+
|
|
5
|
+
const app = express();
|
|
6
|
+
const ollama = new Ollama({ host: "http://localhost:11434" });
|
|
7
|
+
const chroma = new ChromaClient();
|
|
8
|
+
|
|
9
|
+
app.post("/chat", async (req, res) => {
|
|
10
|
+
const collection = await chroma.getCollection({ name: "docs" });
|
|
11
|
+
const results = await collection.query({ queryTexts: [req.body.query] });
|
|
12
|
+
const context = results.documents.flat().join("\n");
|
|
13
|
+
|
|
14
|
+
const response = await ollama.chat({
|
|
15
|
+
model: "llama3.2",
|
|
16
|
+
messages: [{ role: "user", content: `Context: ${context}\n\nQuestion: ${req.body.query}` }],
|
|
17
|
+
});
|
|
18
|
+
res.json({ answer: response.message.content });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
app.listen(3000);
|
|
@@ -32,8 +32,8 @@ describe("classifyGPAICompliance", () => {
|
|
|
32
32
|
const result = classifyGPAICompliance(answers);
|
|
33
33
|
const flag = result.flags.find((f) => f.obligation === "copyright-policy");
|
|
34
34
|
expect(flag).toBeDefined();
|
|
35
|
-
expect(flag
|
|
36
|
-
expect(flag
|
|
35
|
+
expect(flag?.status).toBe("missing");
|
|
36
|
+
expect(flag?.articleRef).toBe("Art. 53(1)(d)");
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
it("flags copyright-policy as partial when open-source answers 'unsure'", () => {
|
|
@@ -44,8 +44,8 @@ describe("classifyGPAICompliance", () => {
|
|
|
44
44
|
const result = classifyGPAICompliance(answers);
|
|
45
45
|
const flag = result.flags.find((f) => f.obligation === "copyright-policy");
|
|
46
46
|
expect(flag).toBeDefined();
|
|
47
|
-
expect(flag
|
|
48
|
-
expect(flag
|
|
47
|
+
expect(flag?.status).toBe("partial");
|
|
48
|
+
expect(flag?.articleRef).toBe("Art. 53(1)(d)");
|
|
49
49
|
});
|
|
50
50
|
|
|
51
51
|
it("does NOT flag copyright-policy when open-source answers 'yes'", () => {
|
|
@@ -15,7 +15,7 @@ describe("getTimelineForAudiences", () => {
|
|
|
15
15
|
const phases = getTimelineForAudiences(["all", "gpai"]);
|
|
16
16
|
const phase2 = phases.find((p) => p.id === "phase-2-gpai");
|
|
17
17
|
expect(phase2).toBeDefined();
|
|
18
|
-
const gpaiObligation = phase2
|
|
18
|
+
const gpaiObligation = phase2?.obligations.find((o) => o.articleRef === "Art. 53");
|
|
19
19
|
expect(gpaiObligation).toBeDefined();
|
|
20
20
|
});
|
|
21
21
|
|
|
@@ -23,7 +23,7 @@ describe("getTimelineForAudiences", () => {
|
|
|
23
23
|
const phases = getTimelineForAudiences(["all", "gpai"]);
|
|
24
24
|
const phase2 = phases.find((p) => p.id === "phase-2-gpai");
|
|
25
25
|
expect(phase2).toBeDefined();
|
|
26
|
-
const systemicObligation = phase2
|
|
26
|
+
const systemicObligation = phase2?.obligations.find((o) => o.articleRef === "Art. 55");
|
|
27
27
|
expect(systemicObligation).toBeDefined();
|
|
28
28
|
});
|
|
29
29
|
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { scanRepository } from "../../src/scanner/aggregator";
|
|
4
|
+
|
|
5
|
+
const FIXTURES = resolve(__dirname, "../fixtures");
|
|
6
|
+
|
|
7
|
+
describe("scanRepository", () => {
|
|
8
|
+
it("scans nextjs-openai fixture", async () => {
|
|
9
|
+
const result = await scanRepository({
|
|
10
|
+
targetPath: resolve(FIXTURES, "nextjs-openai"),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const signalIds = result.signals.map((s) => s.id);
|
|
14
|
+
|
|
15
|
+
// Should detect Next.js framework
|
|
16
|
+
expect(signalIds.some((id) => id.includes("nextjs") || id.includes("next"))).toBe(true);
|
|
17
|
+
|
|
18
|
+
// Should detect OpenAI provider
|
|
19
|
+
expect(signalIds.some((id) => id.includes("openai"))).toBe(true);
|
|
20
|
+
|
|
21
|
+
// Should detect AI disclosure from README
|
|
22
|
+
expect(result.signals.some((s) => s.category === "compliance-disclosure")).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("scans ollama-rag fixture", async () => {
|
|
26
|
+
const result = await scanRepository({
|
|
27
|
+
targetPath: resolve(FIXTURES, "ollama-rag"),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const signalIds = result.signals.map((s) => s.id);
|
|
31
|
+
const categories = result.signals.map((s) => s.category);
|
|
32
|
+
|
|
33
|
+
// Should detect Express framework
|
|
34
|
+
expect(signalIds.some((id) => id.includes("express"))).toBe(true);
|
|
35
|
+
|
|
36
|
+
// Should detect Ollama (local inference)
|
|
37
|
+
expect(categories).toContain("local-inference");
|
|
38
|
+
|
|
39
|
+
// Should detect ChromaDB (RAG)
|
|
40
|
+
expect(categories).toContain("rag");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("scans plain-webapp fixture", async () => {
|
|
44
|
+
const result = await scanRepository({
|
|
45
|
+
targetPath: resolve(FIXTURES, "plain-webapp"),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Should NOT detect any AI signals
|
|
49
|
+
const aiSignals = result.signals.filter(
|
|
50
|
+
(s) =>
|
|
51
|
+
s.category === "ai-provider" ||
|
|
52
|
+
s.category === "ai-framework" ||
|
|
53
|
+
s.category === "local-inference" ||
|
|
54
|
+
s.category === "ai-model"
|
|
55
|
+
);
|
|
56
|
+
expect(aiSignals).toHaveLength(0);
|
|
57
|
+
|
|
58
|
+
// Should have minimal signals (framework at most)
|
|
59
|
+
expect(result.signals.length).toBeLessThan(10);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("scans compliance-good fixture", async () => {
|
|
63
|
+
const result = await scanRepository({
|
|
64
|
+
targetPath: resolve(FIXTURES, "compliance-good"),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const categories = result.signals.map((s) => s.category);
|
|
68
|
+
|
|
69
|
+
// Should detect compliance signals for disclosure, logging, bias
|
|
70
|
+
expect(categories).toContain("compliance-logging");
|
|
71
|
+
// OpenAI is an AI provider
|
|
72
|
+
expect(categories).toContain("ai-provider");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("generates open questions when AI is detected", async () => {
|
|
76
|
+
const result = await scanRepository({
|
|
77
|
+
targetPath: resolve(FIXTURES, "nextjs-openai"),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(result.openQuestions.length).toBeGreaterThan(0);
|
|
81
|
+
|
|
82
|
+
// Should include the market placement question
|
|
83
|
+
const marketQuestion = result.openQuestions.find((q) => q.id === "oq-market-placement");
|
|
84
|
+
expect(marketQuestion).toBeDefined();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("generates signal summary", async () => {
|
|
88
|
+
const result = await scanRepository({
|
|
89
|
+
targetPath: resolve(FIXTURES, "nextjs-openai"),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(result.signalSummary.total).toBe(result.signals.length);
|
|
93
|
+
expect(result.signalSummary.total).toBeGreaterThan(0);
|
|
94
|
+
|
|
95
|
+
// byConfidence should have correct totals
|
|
96
|
+
const confidenceTotal =
|
|
97
|
+
result.signalSummary.byConfidence.high +
|
|
98
|
+
result.signalSummary.byConfidence.medium +
|
|
99
|
+
result.signalSummary.byConfidence.low;
|
|
100
|
+
expect(confidenceTotal).toBe(result.signalSummary.total);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("deduplicates signals", async () => {
|
|
104
|
+
const result = await scanRepository({
|
|
105
|
+
targetPath: resolve(FIXTURES, "nextjs-openai"),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Check that signal IDs are unique after deduplication
|
|
109
|
+
const ids = result.signals.map((s) => s.id);
|
|
110
|
+
const uniqueIds = new Set(ids);
|
|
111
|
+
expect(ids.length).toBe(uniqueIds.size);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("records scan scope metadata", async () => {
|
|
115
|
+
const result = await scanRepository({
|
|
116
|
+
targetPath: resolve(FIXTURES, "nextjs-openai"),
|
|
117
|
+
scope: "all",
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(result.meta.scanScope).toBe("all");
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { DetectorContext, ScanFile } from "../../src/evidence/types";
|
|
3
|
+
import { detectAiFrameworkSignals } from "../../src/scanner/detectors/ai-framework-detector";
|
|
4
|
+
import { detectAiProviderSignals } from "../../src/scanner/detectors/ai-provider-detector";
|
|
5
|
+
import { detectCompliance } from "../../src/scanner/detectors/compliance-detector";
|
|
6
|
+
import { detectConfig } from "../../src/scanner/detectors/config-detector";
|
|
7
|
+
import { detectFrameworks } from "../../src/scanner/detectors/framework-detector";
|
|
8
|
+
import { detectLocalInferenceSignals } from "../../src/scanner/detectors/local-inference-detector";
|
|
9
|
+
import { detectMarkdown } from "../../src/scanner/detectors/markdown-detector";
|
|
10
|
+
import { detectPackageJsonSignals } from "../../src/scanner/detectors/package-json-detector";
|
|
11
|
+
import { detectRagSignals } from "../../src/scanner/detectors/rag-detector";
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Helper
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
function makeContext(
|
|
18
|
+
file: Partial<ScanFile> & { content: string; relativePath: string }
|
|
19
|
+
): DetectorContext {
|
|
20
|
+
const extension =
|
|
21
|
+
file.extension ??
|
|
22
|
+
(file.relativePath.split(".").pop() ? `.${file.relativePath.split(".").pop()}` : "");
|
|
23
|
+
const provenance =
|
|
24
|
+
file.provenance ??
|
|
25
|
+
(extension === ".md" && !file.relativePath.includes("/") ? "root-docs" : "runtime");
|
|
26
|
+
|
|
27
|
+
const scanFile = {
|
|
28
|
+
relativePath: file.relativePath,
|
|
29
|
+
absolutePath: `/test/${file.relativePath}`,
|
|
30
|
+
extension,
|
|
31
|
+
content: file.content,
|
|
32
|
+
sizeBytes: file.content.length,
|
|
33
|
+
provenance,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
file: scanFile,
|
|
38
|
+
lines: scanFile.content.split("\n"),
|
|
39
|
+
allFiles: [],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// package-json-detector
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
describe("package-json-detector", () => {
|
|
48
|
+
it("detects AI provider and framework signals from package.json", () => {
|
|
49
|
+
const ctx = makeContext({
|
|
50
|
+
relativePath: "package.json",
|
|
51
|
+
extension: ".json",
|
|
52
|
+
content: JSON.stringify({
|
|
53
|
+
dependencies: {
|
|
54
|
+
openai: "^4.0.0",
|
|
55
|
+
next: "^14.0.0",
|
|
56
|
+
},
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const signals = detectPackageJsonSignals(ctx);
|
|
61
|
+
|
|
62
|
+
const categories = signals.map((s) => s.category);
|
|
63
|
+
expect(categories).toContain("ai-provider");
|
|
64
|
+
expect(categories).toContain("framework");
|
|
65
|
+
|
|
66
|
+
const openaiSignal = signals.find((s) => s.category === "ai-provider");
|
|
67
|
+
expect(openaiSignal).toBeDefined();
|
|
68
|
+
expect(openaiSignal?.confidence).toBe("high");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("returns empty for non-package files", () => {
|
|
72
|
+
const ctx = makeContext({
|
|
73
|
+
relativePath: "src/index.ts",
|
|
74
|
+
extension: ".ts",
|
|
75
|
+
content: '{ "openai": "^4.0.0" }',
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(detectPackageJsonSignals(ctx)).toHaveLength(0);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// ai-provider-detector
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
describe("ai-provider-detector", () => {
|
|
87
|
+
it("detects OpenAI import in TypeScript", () => {
|
|
88
|
+
const ctx = makeContext({
|
|
89
|
+
relativePath: "src/ai.ts",
|
|
90
|
+
extension: ".ts",
|
|
91
|
+
content: 'import OpenAI from "openai";\n\nconst client = new OpenAI();',
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const signals = detectAiProviderSignals(ctx);
|
|
95
|
+
|
|
96
|
+
expect(signals.length).toBeGreaterThanOrEqual(1);
|
|
97
|
+
expect(signals[0]?.category).toBe("ai-provider");
|
|
98
|
+
expect(signals[0]?.id).toBe("ai-provider-openai");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("ignores non-source files", () => {
|
|
102
|
+
const ctx = makeContext({
|
|
103
|
+
relativePath: "README.md",
|
|
104
|
+
extension: ".md",
|
|
105
|
+
content: 'import OpenAI from "openai";',
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(detectAiProviderSignals(ctx)).toHaveLength(0);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// ai-framework-detector
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
describe("ai-framework-detector", () => {
|
|
117
|
+
it("detects LangChain import", () => {
|
|
118
|
+
const ctx = makeContext({
|
|
119
|
+
relativePath: "src/chain.ts",
|
|
120
|
+
extension: ".ts",
|
|
121
|
+
content: 'import { ChatOpenAI } from "langchain/chat_models";\n',
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const signals = detectAiFrameworkSignals(ctx);
|
|
125
|
+
|
|
126
|
+
expect(signals.length).toBeGreaterThanOrEqual(1);
|
|
127
|
+
expect(signals[0]?.category).toBe("ai-framework");
|
|
128
|
+
expect(signals[0]?.id).toContain("langchain");
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// local-inference-detector
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
describe("local-inference-detector", () => {
|
|
137
|
+
it("detects Ollama localhost URL", () => {
|
|
138
|
+
const ctx = makeContext({
|
|
139
|
+
relativePath: "src/llm.ts",
|
|
140
|
+
extension: ".ts",
|
|
141
|
+
content: 'new Ollama({ host: "http://localhost:11434" })',
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const signals = detectLocalInferenceSignals(ctx);
|
|
145
|
+
|
|
146
|
+
expect(signals.length).toBeGreaterThanOrEqual(1);
|
|
147
|
+
expect(signals.some((s) => s.category === "local-inference")).toBe(true);
|
|
148
|
+
// localhost:11434 is a high-confidence signal
|
|
149
|
+
const ollamaSignal = signals.find((s) => s.id.includes("ollama-api"));
|
|
150
|
+
expect(ollamaSignal).toBeDefined();
|
|
151
|
+
expect(ollamaSignal?.confidence).toBe("high");
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// rag-detector
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
describe("rag-detector", () => {
|
|
160
|
+
it("detects ChromaDB import", () => {
|
|
161
|
+
const ctx = makeContext({
|
|
162
|
+
relativePath: "src/rag.ts",
|
|
163
|
+
extension: ".ts",
|
|
164
|
+
content: 'import { ChromaClient } from "chromadb";\n\nconst chroma = new ChromaClient();',
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const signals = detectRagSignals(ctx);
|
|
168
|
+
|
|
169
|
+
expect(signals.length).toBeGreaterThanOrEqual(1);
|
|
170
|
+
expect(signals.some((s) => s.category === "rag")).toBe(true);
|
|
171
|
+
const chromaSignal = signals.find((s) => s.id === "rag-chromadb");
|
|
172
|
+
expect(chromaSignal).toBeDefined();
|
|
173
|
+
expect(chromaSignal?.confidence).toBe("high");
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// framework-detector
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
describe("framework-detector", () => {
|
|
182
|
+
it("detects Next.js from config file", () => {
|
|
183
|
+
const ctx = makeContext({
|
|
184
|
+
relativePath: "next.config.ts",
|
|
185
|
+
extension: ".ts",
|
|
186
|
+
content: "export default { reactStrictMode: true };",
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const signals = detectFrameworks(ctx);
|
|
190
|
+
|
|
191
|
+
expect(signals.length).toBeGreaterThanOrEqual(1);
|
|
192
|
+
const nextSignal = signals.find((s) => s.id === "framework-nextjs");
|
|
193
|
+
expect(nextSignal).toBeDefined();
|
|
194
|
+
expect(nextSignal?.category).toBe("framework");
|
|
195
|
+
expect(nextSignal?.confidence).toBe("high");
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// compliance-detector
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
describe("compliance-detector", () => {
|
|
204
|
+
it("detects AI disclosure text and logging library", () => {
|
|
205
|
+
const ctx = makeContext({
|
|
206
|
+
relativePath: "app/page.tsx",
|
|
207
|
+
extension: ".tsx",
|
|
208
|
+
content: [
|
|
209
|
+
'import pino from "pino";',
|
|
210
|
+
"const logger = pino();",
|
|
211
|
+
'// This response was "generated by AI"',
|
|
212
|
+
'logger.info({ action: "ai-response" }, "done");',
|
|
213
|
+
].join("\n"),
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const signals = detectCompliance(ctx);
|
|
217
|
+
|
|
218
|
+
const categories = signals.map((s) => s.category);
|
|
219
|
+
expect(categories).toContain("compliance-disclosure");
|
|
220
|
+
expect(categories).toContain("compliance-logging");
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
// config-detector
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
describe("config-detector", () => {
|
|
229
|
+
it("detects AI provider from .env.example", () => {
|
|
230
|
+
const ctx = makeContext({
|
|
231
|
+
relativePath: ".env.example",
|
|
232
|
+
extension: "",
|
|
233
|
+
content: "OPENAI_API_KEY=\nDATABASE_URL=postgres://localhost/db\n",
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const signals = detectConfig(ctx);
|
|
237
|
+
|
|
238
|
+
expect(signals.length).toBeGreaterThanOrEqual(1);
|
|
239
|
+
const openaiSignal = signals.find((s) => s.category === "ai-provider");
|
|
240
|
+
expect(openaiSignal).toBeDefined();
|
|
241
|
+
expect(openaiSignal?.confidence).toBe("high");
|
|
242
|
+
// Ensure the value is not leaked — only the key name
|
|
243
|
+
expect(openaiSignal?.evidence[0]?.snippet).not.toContain("=");
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
// markdown-detector
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
describe("markdown-detector", () => {
|
|
252
|
+
it("detects AI disclosure in README", () => {
|
|
253
|
+
const ctx = makeContext({
|
|
254
|
+
relativePath: "README.md",
|
|
255
|
+
extension: ".md",
|
|
256
|
+
content: "# My App\n\nThis app uses artificial intelligence to generate responses.\n",
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const signals = detectMarkdown(ctx);
|
|
260
|
+
|
|
261
|
+
expect(signals.length).toBeGreaterThanOrEqual(1);
|
|
262
|
+
expect(signals.some((s) => s.category === "compliance-disclosure")).toBe(true);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("ignores non-markdown files", () => {
|
|
266
|
+
const ctx = makeContext({
|
|
267
|
+
relativePath: "src/index.ts",
|
|
268
|
+
extension: ".ts",
|
|
269
|
+
content: "This app uses artificial intelligence",
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
expect(detectMarkdown(ctx)).toHaveLength(0);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { discoverFiles } from "../../src/scanner/discovery";
|
|
4
|
+
|
|
5
|
+
const FIXTURES = resolve(__dirname, "../fixtures");
|
|
6
|
+
|
|
7
|
+
describe("discoverFiles", () => {
|
|
8
|
+
it("discovers files in a fixture directory", async () => {
|
|
9
|
+
const { files } = await discoverFiles({
|
|
10
|
+
targetPath: resolve(FIXTURES, "nextjs-openai"),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const paths = files.map((f) => f.relativePath);
|
|
14
|
+
expect(paths).toContain("package.json");
|
|
15
|
+
expect(paths.some((p) => p.includes("app/page.tsx"))).toBe(true);
|
|
16
|
+
expect(paths).toContain("README.md");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("respects extension filtering", async () => {
|
|
20
|
+
const { files } = await discoverFiles({
|
|
21
|
+
targetPath: resolve(FIXTURES, "nextjs-openai"),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// All discovered files should match default extensions or named file patterns
|
|
25
|
+
for (const file of files) {
|
|
26
|
+
const ext = file.extension;
|
|
27
|
+
const isDefaultExt = [".ts", ".tsx", ".js", ".jsx", ".json", ".md", ".yaml", ".yml"].includes(
|
|
28
|
+
ext
|
|
29
|
+
);
|
|
30
|
+
const isNamedFile =
|
|
31
|
+
/^(package\.json|\.env\.|next\.config\.|vite\.config\.|docker-compose|Dockerfile|tsconfig)/.test(
|
|
32
|
+
file.relativePath.split("/").pop() ?? ""
|
|
33
|
+
);
|
|
34
|
+
expect(isDefaultExt || isNamedFile).toBe(true);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("skips node_modules", async () => {
|
|
39
|
+
const { files } = await discoverFiles({
|
|
40
|
+
targetPath: resolve(FIXTURES, "nextjs-openai"),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const hasNodeModules = files.some((f) => f.relativePath.includes("node_modules"));
|
|
44
|
+
expect(hasNodeModules).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("defaults to production scope and excludes test/example files", async () => {
|
|
48
|
+
const { files } = await discoverFiles({
|
|
49
|
+
targetPath: resolve(FIXTURES, "compliance-good"),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const paths = files.map((file) => file.relativePath);
|
|
53
|
+
expect(paths.some((path) => path.includes("tests/"))).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("includes test files in all scope and annotates provenance", async () => {
|
|
57
|
+
const { files } = await discoverFiles({
|
|
58
|
+
targetPath: resolve(FIXTURES, "compliance-good"),
|
|
59
|
+
scope: "all",
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const testFile = files.find((file) => file.relativePath.includes("tests/bias-check.test.ts"));
|
|
63
|
+
expect(testFile).toBeDefined();
|
|
64
|
+
expect(testFile?.provenance).toBe("test");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("respects max file size", async () => {
|
|
68
|
+
// Set a very small max file size — only tiny files should be included
|
|
69
|
+
const { files } = await discoverFiles({
|
|
70
|
+
targetPath: resolve(FIXTURES, "nextjs-openai"),
|
|
71
|
+
maxFileSizeBytes: 10,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// All included files should be at most 10 bytes
|
|
75
|
+
for (const file of files) {
|
|
76
|
+
expect(file.sizeBytes).toBeLessThanOrEqual(10);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("handles non-existent path", async () => {
|
|
81
|
+
await expect(
|
|
82
|
+
discoverFiles({
|
|
83
|
+
targetPath: resolve(FIXTURES, "this-does-not-exist"),
|
|
84
|
+
})
|
|
85
|
+
).rejects.toThrow(/Cannot read directory/);
|
|
86
|
+
});
|
|
87
|
+
});
|
package/vitest.config.ts
CHANGED
|
@@ -5,6 +5,7 @@ export default defineConfig({
|
|
|
5
5
|
globals: true,
|
|
6
6
|
environment: "node",
|
|
7
7
|
include: ["tests/**/*.test.ts"],
|
|
8
|
+
exclude: ["tests/fixtures/**"],
|
|
8
9
|
// Work around rare worker-pool teardown issues (tinypool) on some Node/tooling combos.
|
|
9
10
|
// Core tests are lightweight; single-thread execution improves stability and reproducibility.
|
|
10
11
|
pool: "threads",
|