@euconform/core 1.2.0 → 1.3.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.
Files changed (121) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +6 -0
  3. package/dist/evidence/bundle.d.ts +23 -0
  4. package/dist/evidence/bundle.d.ts.map +1 -0
  5. package/dist/evidence/bundle.js +44 -0
  6. package/dist/evidence/compliance-evaluation.d.ts +12 -0
  7. package/dist/evidence/compliance-evaluation.d.ts.map +1 -0
  8. package/dist/evidence/compliance-evaluation.js +78 -0
  9. package/dist/evidence/hash.d.ts +2 -0
  10. package/dist/evidence/hash.d.ts.map +1 -0
  11. package/dist/evidence/hash.js +4 -0
  12. package/dist/evidence/index.d.ts +13 -0
  13. package/dist/evidence/index.d.ts.map +1 -0
  14. package/dist/evidence/index.js +11 -0
  15. package/dist/evidence/markdown.d.ts +9 -0
  16. package/dist/evidence/markdown.d.ts.map +1 -0
  17. package/dist/evidence/markdown.js +169 -0
  18. package/dist/evidence/output.d.ts +13 -0
  19. package/dist/evidence/output.d.ts.map +1 -0
  20. package/dist/evidence/output.js +547 -0
  21. package/dist/evidence/types.d.ts +230 -0
  22. package/dist/evidence/types.d.ts.map +1 -0
  23. package/dist/evidence/types.js +7 -0
  24. package/dist/evidence/validate.d.ts +8 -0
  25. package/dist/evidence/validate.d.ts.map +1 -0
  26. package/dist/evidence/validate.js +175 -0
  27. package/dist/inference/ollama-client.d.ts +2 -0
  28. package/dist/inference/ollama-client.d.ts.map +1 -1
  29. package/dist/inference/ollama-client.js +61 -73
  30. package/dist/scanner/aggregator.d.ts +7 -0
  31. package/dist/scanner/aggregator.d.ts.map +1 -0
  32. package/dist/scanner/aggregator.js +200 -0
  33. package/dist/scanner/detectors/ai-framework-detector.d.ts +3 -0
  34. package/dist/scanner/detectors/ai-framework-detector.d.ts.map +1 -0
  35. package/dist/scanner/detectors/ai-framework-detector.js +74 -0
  36. package/dist/scanner/detectors/ai-provider-detector.d.ts +3 -0
  37. package/dist/scanner/detectors/ai-provider-detector.d.ts.map +1 -0
  38. package/dist/scanner/detectors/ai-provider-detector.js +100 -0
  39. package/dist/scanner/detectors/compliance-detector.d.ts +3 -0
  40. package/dist/scanner/detectors/compliance-detector.d.ts.map +1 -0
  41. package/dist/scanner/detectors/compliance-detector.js +201 -0
  42. package/dist/scanner/detectors/config-detector.d.ts +3 -0
  43. package/dist/scanner/detectors/config-detector.d.ts.map +1 -0
  44. package/dist/scanner/detectors/config-detector.js +248 -0
  45. package/dist/scanner/detectors/framework-detector.d.ts +3 -0
  46. package/dist/scanner/detectors/framework-detector.d.ts.map +1 -0
  47. package/dist/scanner/detectors/framework-detector.js +141 -0
  48. package/dist/scanner/detectors/index.d.ts +8 -0
  49. package/dist/scanner/detectors/index.d.ts.map +1 -0
  50. package/dist/scanner/detectors/index.js +37 -0
  51. package/dist/scanner/detectors/local-inference-detector.d.ts +3 -0
  52. package/dist/scanner/detectors/local-inference-detector.d.ts.map +1 -0
  53. package/dist/scanner/detectors/local-inference-detector.js +93 -0
  54. package/dist/scanner/detectors/markdown-detector.d.ts +3 -0
  55. package/dist/scanner/detectors/markdown-detector.d.ts.map +1 -0
  56. package/dist/scanner/detectors/markdown-detector.js +139 -0
  57. package/dist/scanner/detectors/package-json-detector.d.ts +3 -0
  58. package/dist/scanner/detectors/package-json-detector.d.ts.map +1 -0
  59. package/dist/scanner/detectors/package-json-detector.js +166 -0
  60. package/dist/scanner/detectors/rag-detector.d.ts +3 -0
  61. package/dist/scanner/detectors/rag-detector.d.ts.map +1 -0
  62. package/dist/scanner/detectors/rag-detector.js +132 -0
  63. package/dist/scanner/detectors/shared.d.ts +18 -0
  64. package/dist/scanner/detectors/shared.d.ts.map +1 -0
  65. package/dist/scanner/detectors/shared.js +36 -0
  66. package/dist/scanner/discovery.d.ts +14 -0
  67. package/dist/scanner/discovery.d.ts.map +1 -0
  68. package/dist/scanner/discovery.js +235 -0
  69. package/dist/scanner/index.d.ts +9 -0
  70. package/dist/scanner/index.d.ts.map +1 -0
  71. package/dist/scanner/index.js +8 -0
  72. package/package.json +6 -3
  73. package/src/evidence/bundle.ts +65 -0
  74. package/src/evidence/compliance-evaluation.ts +114 -0
  75. package/src/evidence/hash.ts +5 -0
  76. package/src/evidence/index.ts +51 -0
  77. package/src/evidence/markdown.ts +226 -0
  78. package/src/evidence/output.ts +717 -0
  79. package/src/evidence/types.ts +329 -0
  80. package/src/evidence/validate.ts +270 -0
  81. package/src/inference/ollama-client.ts +81 -90
  82. package/src/scanner/aggregator.ts +258 -0
  83. package/src/scanner/detectors/ai-framework-detector.ts +94 -0
  84. package/src/scanner/detectors/ai-provider-detector.ts +135 -0
  85. package/src/scanner/detectors/compliance-detector.ts +232 -0
  86. package/src/scanner/detectors/config-detector.ts +316 -0
  87. package/src/scanner/detectors/framework-detector.ts +163 -0
  88. package/src/scanner/detectors/index.ts +40 -0
  89. package/src/scanner/detectors/local-inference-detector.ts +124 -0
  90. package/src/scanner/detectors/markdown-detector.ts +176 -0
  91. package/src/scanner/detectors/package-json-detector.ts +202 -0
  92. package/src/scanner/detectors/rag-detector.ts +162 -0
  93. package/src/scanner/detectors/shared.ts +45 -0
  94. package/src/scanner/discovery.ts +290 -0
  95. package/src/scanner/index.ts +9 -0
  96. package/tests/evidence/bundle.test.ts +128 -0
  97. package/tests/evidence/ecef-schemas.test.ts +316 -0
  98. package/tests/evidence/hash.test.ts +25 -0
  99. package/tests/evidence/markdown.test.ts +92 -0
  100. package/tests/evidence/output.test.ts +203 -0
  101. package/tests/fixtures/compliance-good/README.md +14 -0
  102. package/tests/fixtures/compliance-good/lib/ai-service.ts +17 -0
  103. package/tests/fixtures/compliance-good/lib/review-flow.ts +11 -0
  104. package/tests/fixtures/compliance-good/package.json +10 -0
  105. package/tests/fixtures/compliance-good/tests/bias-check.test.ts +13 -0
  106. package/tests/fixtures/nextjs-openai/README.md +5 -0
  107. package/tests/fixtures/nextjs-openai/app/page.tsx +11 -0
  108. package/tests/fixtures/nextjs-openai/package.json +9 -0
  109. package/tests/fixtures/ollama-rag/.env.example +2 -0
  110. package/tests/fixtures/ollama-rag/docker-compose.yml +9 -0
  111. package/tests/fixtures/ollama-rag/package.json +9 -0
  112. package/tests/fixtures/ollama-rag/server.ts +21 -0
  113. package/tests/fixtures/plain-webapp/README.md +3 -0
  114. package/tests/fixtures/plain-webapp/index.js +5 -0
  115. package/tests/fixtures/plain-webapp/package.json +8 -0
  116. package/tests/gpai-classifier.test.ts +4 -4
  117. package/tests/gpai-timeline.test.ts +2 -2
  118. package/tests/scanner/aggregator.test.ts +122 -0
  119. package/tests/scanner/detectors.test.ts +274 -0
  120. package/tests/scanner/discovery.test.ts +87 -0
  121. package/vitest.config.ts +1 -0
@@ -0,0 +1,13 @@
1
+ import { disparateImpact, statisticalParityDifference } from "@euconform/core";
2
+
3
+ describe("Bias Testing", () => {
4
+ it("should check disparate impact", () => {
5
+ const result = disparateImpact([]);
6
+ expect(result).toBeDefined();
7
+ });
8
+
9
+ it("should check statistical parity", () => {
10
+ const result = statisticalParityDifference([]);
11
+ expect(result).toBeDefined();
12
+ });
13
+ });
@@ -0,0 +1,5 @@
1
+ # My AI App
2
+
3
+ This application uses AI to generate responses.
4
+
5
+ **Note:** This app is powered by artificial intelligence. Responses are AI-generated.
@@ -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,9 @@
1
+ {
2
+ "name": "my-nextjs-ai-app",
3
+ "version": "1.0.0",
4
+ "dependencies": {
5
+ "next": "^14.0.0",
6
+ "react": "^18.0.0",
7
+ "openai": "^4.0.0"
8
+ }
9
+ }
@@ -0,0 +1,2 @@
1
+ OLLAMA_HOST=http://localhost:11434
2
+ CHROMA_HOST=http://localhost:8000
@@ -0,0 +1,9 @@
1
+ services:
2
+ ollama:
3
+ image: ollama/ollama:latest
4
+ ports:
5
+ - "11434:11434"
6
+ chromadb:
7
+ image: chromadb/chroma:latest
8
+ ports:
9
+ - "8000:8000"
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "ollama-rag-server",
3
+ "version": "0.1.0",
4
+ "dependencies": {
5
+ "express": "^4.18.0",
6
+ "ollama": "^0.5.0",
7
+ "chromadb": "^1.8.0"
8
+ }
9
+ }
@@ -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);
@@ -0,0 +1,3 @@
1
+ # Plain Web App
2
+
3
+ A simple web application with no AI components.
@@ -0,0 +1,5 @@
1
+ const express = require("express");
2
+ const app = express();
3
+ app.set("view engine", "ejs");
4
+ app.get("/", (_req, res) => res.render("index", { title: "Hello World" }));
5
+ app.listen(3000);
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "plain-webapp",
3
+ "version": "1.0.0",
4
+ "dependencies": {
5
+ "express": "^4.18.0",
6
+ "ejs": "^3.1.0"
7
+ }
8
+ }
@@ -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!.status).toBe("missing");
36
- expect(flag!.articleRef).toBe("Art. 53(1)(d)");
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!.status).toBe("partial");
48
- expect(flag!.articleRef).toBe("Art. 53(1)(d)");
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!.obligations.find((o) => o.articleRef === "Art. 53");
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!.obligations.find((o) => o.articleRef === "Art. 55");
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",