@euconform/core 1.0.1 → 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 (183) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +12 -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/index.d.ts +2 -2
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +1 -1
  30. package/dist/inference/ollama-client.d.ts +3 -0
  31. package/dist/inference/ollama-client.d.ts.map +1 -1
  32. package/dist/inference/ollama-client.js +69 -71
  33. package/dist/legal-checks/annex-iv-report.d.ts +3 -0
  34. package/dist/legal-checks/annex-iv-report.d.ts.map +1 -1
  35. package/dist/legal-checks/annex-iv-report.js +1 -0
  36. package/dist/legal-checks/deadline-timeline.d.ts +57 -0
  37. package/dist/legal-checks/deadline-timeline.d.ts.map +1 -0
  38. package/dist/legal-checks/deadline-timeline.js +265 -0
  39. package/dist/legal-checks/gap-analysis.d.ts +48 -0
  40. package/dist/legal-checks/gap-analysis.d.ts.map +1 -0
  41. package/dist/legal-checks/gap-analysis.js +397 -0
  42. package/dist/legal-checks/gpai-classifier.d.ts +49 -0
  43. package/dist/legal-checks/gpai-classifier.d.ts.map +1 -0
  44. package/dist/legal-checks/gpai-classifier.js +197 -0
  45. package/dist/legal-checks/index.d.ts +6 -0
  46. package/dist/legal-checks/index.d.ts.map +1 -1
  47. package/dist/legal-checks/index.js +3 -0
  48. package/dist/scanner/aggregator.d.ts +7 -0
  49. package/dist/scanner/aggregator.d.ts.map +1 -0
  50. package/dist/scanner/aggregator.js +200 -0
  51. package/dist/scanner/detectors/ai-framework-detector.d.ts +3 -0
  52. package/dist/scanner/detectors/ai-framework-detector.d.ts.map +1 -0
  53. package/dist/scanner/detectors/ai-framework-detector.js +74 -0
  54. package/dist/scanner/detectors/ai-provider-detector.d.ts +3 -0
  55. package/dist/scanner/detectors/ai-provider-detector.d.ts.map +1 -0
  56. package/dist/scanner/detectors/ai-provider-detector.js +100 -0
  57. package/dist/scanner/detectors/compliance-detector.d.ts +3 -0
  58. package/dist/scanner/detectors/compliance-detector.d.ts.map +1 -0
  59. package/dist/scanner/detectors/compliance-detector.js +201 -0
  60. package/dist/scanner/detectors/config-detector.d.ts +3 -0
  61. package/dist/scanner/detectors/config-detector.d.ts.map +1 -0
  62. package/dist/scanner/detectors/config-detector.js +248 -0
  63. package/dist/scanner/detectors/framework-detector.d.ts +3 -0
  64. package/dist/scanner/detectors/framework-detector.d.ts.map +1 -0
  65. package/dist/scanner/detectors/framework-detector.js +141 -0
  66. package/dist/scanner/detectors/index.d.ts +8 -0
  67. package/dist/scanner/detectors/index.d.ts.map +1 -0
  68. package/dist/scanner/detectors/index.js +37 -0
  69. package/dist/scanner/detectors/local-inference-detector.d.ts +3 -0
  70. package/dist/scanner/detectors/local-inference-detector.d.ts.map +1 -0
  71. package/dist/scanner/detectors/local-inference-detector.js +93 -0
  72. package/dist/scanner/detectors/markdown-detector.d.ts +3 -0
  73. package/dist/scanner/detectors/markdown-detector.d.ts.map +1 -0
  74. package/dist/scanner/detectors/markdown-detector.js +139 -0
  75. package/dist/scanner/detectors/package-json-detector.d.ts +3 -0
  76. package/dist/scanner/detectors/package-json-detector.d.ts.map +1 -0
  77. package/dist/scanner/detectors/package-json-detector.js +166 -0
  78. package/dist/scanner/detectors/rag-detector.d.ts +3 -0
  79. package/dist/scanner/detectors/rag-detector.d.ts.map +1 -0
  80. package/dist/scanner/detectors/rag-detector.js +132 -0
  81. package/dist/scanner/detectors/shared.d.ts +18 -0
  82. package/dist/scanner/detectors/shared.d.ts.map +1 -0
  83. package/dist/scanner/detectors/shared.js +36 -0
  84. package/dist/scanner/discovery.d.ts +14 -0
  85. package/dist/scanner/discovery.d.ts.map +1 -0
  86. package/dist/scanner/discovery.js +235 -0
  87. package/dist/scanner/index.d.ts +9 -0
  88. package/dist/scanner/index.d.ts.map +1 -0
  89. package/dist/scanner/index.js +8 -0
  90. package/package.json +6 -3
  91. package/src/evidence/bundle.ts +65 -0
  92. package/src/evidence/compliance-evaluation.ts +114 -0
  93. package/src/evidence/hash.ts +5 -0
  94. package/src/evidence/index.ts +51 -0
  95. package/src/evidence/markdown.ts +226 -0
  96. package/src/evidence/output.ts +717 -0
  97. package/src/evidence/types.ts +329 -0
  98. package/src/evidence/validate.ts +270 -0
  99. package/src/index.ts +25 -0
  100. package/src/inference/ollama-client.ts +93 -86
  101. package/src/legal-checks/annex-iv-report.ts +4 -0
  102. package/src/legal-checks/deadline-timeline.ts +313 -0
  103. package/src/legal-checks/gap-analysis.ts +454 -0
  104. package/src/legal-checks/gpai-classifier.ts +281 -0
  105. package/src/legal-checks/index.ts +33 -0
  106. package/src/scanner/aggregator.ts +258 -0
  107. package/src/scanner/detectors/ai-framework-detector.ts +94 -0
  108. package/src/scanner/detectors/ai-provider-detector.ts +135 -0
  109. package/src/scanner/detectors/compliance-detector.ts +232 -0
  110. package/src/scanner/detectors/config-detector.ts +316 -0
  111. package/src/scanner/detectors/framework-detector.ts +163 -0
  112. package/src/scanner/detectors/index.ts +40 -0
  113. package/src/scanner/detectors/local-inference-detector.ts +124 -0
  114. package/src/scanner/detectors/markdown-detector.ts +176 -0
  115. package/src/scanner/detectors/package-json-detector.ts +202 -0
  116. package/src/scanner/detectors/rag-detector.ts +162 -0
  117. package/src/scanner/detectors/shared.ts +45 -0
  118. package/src/scanner/discovery.ts +290 -0
  119. package/src/scanner/index.ts +9 -0
  120. package/tests/evidence/bundle.test.ts +128 -0
  121. package/tests/evidence/ecef-schemas.test.ts +316 -0
  122. package/tests/evidence/hash.test.ts +25 -0
  123. package/tests/evidence/markdown.test.ts +92 -0
  124. package/tests/evidence/output.test.ts +203 -0
  125. package/tests/fixtures/compliance-good/README.md +14 -0
  126. package/tests/fixtures/compliance-good/lib/ai-service.ts +17 -0
  127. package/tests/fixtures/compliance-good/lib/review-flow.ts +11 -0
  128. package/tests/fixtures/compliance-good/package.json +10 -0
  129. package/tests/fixtures/compliance-good/tests/bias-check.test.ts +13 -0
  130. package/tests/fixtures/nextjs-openai/README.md +5 -0
  131. package/tests/fixtures/nextjs-openai/app/page.tsx +11 -0
  132. package/tests/fixtures/nextjs-openai/package.json +9 -0
  133. package/tests/fixtures/ollama-rag/.env.example +2 -0
  134. package/tests/fixtures/ollama-rag/docker-compose.yml +9 -0
  135. package/tests/fixtures/ollama-rag/package.json +9 -0
  136. package/tests/fixtures/ollama-rag/server.ts +21 -0
  137. package/tests/fixtures/plain-webapp/README.md +3 -0
  138. package/tests/fixtures/plain-webapp/index.js +5 -0
  139. package/tests/fixtures/plain-webapp/package.json +8 -0
  140. package/tests/gpai-classifier.test.ts +125 -0
  141. package/tests/gpai-timeline.test.ts +55 -0
  142. package/tests/scanner/aggregator.test.ts +122 -0
  143. package/tests/scanner/detectors.test.ts +274 -0
  144. package/tests/scanner/discovery.test.ts +87 -0
  145. package/vitest.config.ts +1 -0
  146. package/.turbo/turbo-check-types.log +0 -4
  147. package/.turbo/turbo-test.log +0 -364
  148. package/coverage/base.css +0 -224
  149. package/coverage/block-navigation.js +0 -87
  150. package/coverage/clover.xml +0 -3282
  151. package/coverage/coverage-final.json +0 -20
  152. package/coverage/favicon.png +0 -0
  153. package/coverage/index.html +0 -161
  154. package/coverage/prettify.css +0 -1
  155. package/coverage/prettify.js +0 -2
  156. package/coverage/sort-arrow-sprite.png +0 -0
  157. package/coverage/sorter.js +0 -210
  158. package/coverage/src/fairness-metrics.ts.html +0 -1579
  159. package/coverage/src/index.html +0 -161
  160. package/coverage/src/index.ts.html +0 -535
  161. package/coverage/src/inference/browser-inference.ts.html +0 -859
  162. package/coverage/src/inference/capability-detection.ts.html +0 -2227
  163. package/coverage/src/inference/index.html +0 -176
  164. package/coverage/src/inference/index.ts.html +0 -553
  165. package/coverage/src/inference/inference-factory.ts.html +0 -751
  166. package/coverage/src/inference/ollama-client.ts.html +0 -1627
  167. package/coverage/src/legal-checks/annex-iv-report.ts.html +0 -556
  168. package/coverage/src/legal-checks/bias-metrics.ts.html +0 -1855
  169. package/coverage/src/legal-checks/data-governance.ts.html +0 -529
  170. package/coverage/src/legal-checks/human-oversight-logging.ts.html +0 -490
  171. package/coverage/src/legal-checks/index.html +0 -206
  172. package/coverage/src/legal-checks/index.ts.html +0 -250
  173. package/coverage/src/legal-checks/risk-classifier.ts.html +0 -940
  174. package/coverage/src/legal-checks/robustness-transparency.ts.html +0 -571
  175. package/coverage/src/metrics/crows-pairs-bias.ts.html +0 -820
  176. package/coverage/src/metrics/index.html +0 -146
  177. package/coverage/src/metrics/index.ts.html +0 -151
  178. package/coverage/src/metrics/stereotype-bias.ts.html +0 -886
  179. package/coverage/src/risk-engine.ts.html +0 -1660
  180. package/coverage/src/types.ts.html +0 -547
  181. package/dist/legal-checks/disclaimer.d.ts +0 -2
  182. package/dist/legal-checks/disclaimer.d.ts.map +0 -1
  183. package/dist/legal-checks/disclaimer.js +0 -1
@@ -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
+ }
@@ -0,0 +1,125 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { classifyGPAICompliance, isSystemicRiskModel } from "../src/legal-checks/gpai-classifier";
3
+ import type { QuizAnswer } from "../src/types";
4
+
5
+ /** Helper: build a full set of GPAI answers with defaults, then override specific ones */
6
+ function buildAnswers(overrides: Record<string, string> = {}): QuizAnswer[] {
7
+ const defaults: Record<string, string> = {
8
+ "gpai-technical-docs": "yes",
9
+ "gpai-downstream-info": "yes",
10
+ "gpai-copyright-policy": "yes",
11
+ "gpai-eu-database": "yes",
12
+ "gpai-open-source": "no",
13
+ "gpai-systemic-risk": "no",
14
+ "gpai-red-teaming": "yes",
15
+ "gpai-incident-reporting": "yes",
16
+ "gpai-cybersecurity": "yes",
17
+ };
18
+ const merged = { ...defaults, ...overrides };
19
+ return Object.entries(merged).map(([questionId, value]) => ({
20
+ questionId,
21
+ value,
22
+ }));
23
+ }
24
+
25
+ describe("classifyGPAICompliance", () => {
26
+ describe("open-source path (Art. 53(2))", () => {
27
+ it("flags copyright-policy as missing when open-source answers 'no'", () => {
28
+ const answers = buildAnswers({
29
+ "gpai-open-source": "yes",
30
+ "gpai-copyright-policy": "no",
31
+ });
32
+ const result = classifyGPAICompliance(answers);
33
+ const flag = result.flags.find((f) => f.obligation === "copyright-policy");
34
+ expect(flag).toBeDefined();
35
+ expect(flag?.status).toBe("missing");
36
+ expect(flag?.articleRef).toBe("Art. 53(1)(d)");
37
+ });
38
+
39
+ it("flags copyright-policy as partial when open-source answers 'unsure'", () => {
40
+ const answers = buildAnswers({
41
+ "gpai-open-source": "yes",
42
+ "gpai-copyright-policy": "unsure",
43
+ });
44
+ const result = classifyGPAICompliance(answers);
45
+ const flag = result.flags.find((f) => f.obligation === "copyright-policy");
46
+ expect(flag).toBeDefined();
47
+ expect(flag?.status).toBe("partial");
48
+ expect(flag?.articleRef).toBe("Art. 53(1)(d)");
49
+ });
50
+
51
+ it("does NOT flag copyright-policy when open-source answers 'yes'", () => {
52
+ const answers = buildAnswers({
53
+ "gpai-open-source": "yes",
54
+ "gpai-copyright-policy": "yes",
55
+ });
56
+ const result = classifyGPAICompliance(answers);
57
+ const flag = result.flags.find((f) => f.obligation === "copyright-policy");
58
+ expect(flag).toBeUndefined();
59
+ });
60
+
61
+ it("exempts technical-docs and downstream-info for open-source", () => {
62
+ const answers = buildAnswers({
63
+ "gpai-open-source": "yes",
64
+ "gpai-technical-docs": "no",
65
+ "gpai-downstream-info": "no",
66
+ });
67
+ const result = classifyGPAICompliance(answers);
68
+ expect(result.flags.find((f) => f.obligation === "technical-docs")).toBeUndefined();
69
+ expect(result.flags.find((f) => f.obligation === "downstream-info")).toBeUndefined();
70
+ });
71
+
72
+ it("includes explanatory note for open-source copyright gap", () => {
73
+ const answers = buildAnswers({
74
+ "gpai-open-source": "yes",
75
+ "gpai-copyright-policy": "unsure",
76
+ });
77
+ const result = classifyGPAICompliance(answers);
78
+ expect(result.notes.some((n) => n.includes("Art. 53(1)(d)"))).toBe(true);
79
+ });
80
+ });
81
+
82
+ describe("non-open-source path", () => {
83
+ it("flags all three Art. 53(1) obligations when missing", () => {
84
+ const answers = buildAnswers({
85
+ "gpai-technical-docs": "no",
86
+ "gpai-downstream-info": "no",
87
+ "gpai-copyright-policy": "no",
88
+ "gpai-eu-database": "no",
89
+ });
90
+ const result = classifyGPAICompliance(answers);
91
+ expect(result.level).toBe("non-compliant");
92
+ expect(result.flags.length).toBeGreaterThanOrEqual(4);
93
+ });
94
+ });
95
+
96
+ describe("systemic risk (Art. 51)", () => {
97
+ it("detects systemic risk model", () => {
98
+ const answers = buildAnswers({ "gpai-systemic-risk": "yes" });
99
+ expect(isSystemicRiskModel(answers)).toBe(true);
100
+ });
101
+
102
+ it("flags Art. 55 obligations when systemic risk and missing", () => {
103
+ const answers = buildAnswers({
104
+ "gpai-systemic-risk": "yes",
105
+ "gpai-red-teaming": "no",
106
+ "gpai-incident-reporting": "no",
107
+ "gpai-cybersecurity": "no",
108
+ });
109
+ const result = classifyGPAICompliance(answers);
110
+ expect(result.isSystemicRisk).toBe(true);
111
+ expect(result.flags.find((f) => f.obligation === "red-teaming")).toBeDefined();
112
+ expect(result.flags.find((f) => f.obligation === "incident-reporting")).toBeDefined();
113
+ expect(result.flags.find((f) => f.obligation === "cybersecurity")).toBeDefined();
114
+ });
115
+ });
116
+
117
+ describe("fully compliant", () => {
118
+ it("returns compliant with no flags when all obligations met", () => {
119
+ const answers = buildAnswers();
120
+ const result = classifyGPAICompliance(answers);
121
+ expect(result.level).toBe("compliant");
122
+ expect(result.flags).toHaveLength(0);
123
+ });
124
+ });
125
+ });
@@ -0,0 +1,55 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { getTimelineForRiskLevel } from "../src/legal-checks/deadline-timeline";
3
+
4
+ // This function will be implemented in Step 3
5
+ import { getTimelineForAudiences } from "../src/legal-checks/deadline-timeline";
6
+
7
+ describe("getTimelineForAudiences", () => {
8
+ it("returns GPAI-relevant phases for ['all', 'gpai'] audiences", () => {
9
+ const phases = getTimelineForAudiences(["all", "gpai"]);
10
+ const phaseIds = phases.map((p) => p.id);
11
+ expect(phaseIds).toContain("phase-2-gpai");
12
+ });
13
+
14
+ it("includes Art. 53 documentation obligation for GPAI audience", () => {
15
+ const phases = getTimelineForAudiences(["all", "gpai"]);
16
+ const phase2 = phases.find((p) => p.id === "phase-2-gpai");
17
+ expect(phase2).toBeDefined();
18
+ const gpaiObligation = phase2?.obligations.find((o) => o.articleRef === "Art. 53");
19
+ expect(gpaiObligation).toBeDefined();
20
+ });
21
+
22
+ it("includes Art. 55 systemic risk obligation for GPAI audience", () => {
23
+ const phases = getTimelineForAudiences(["all", "gpai"]);
24
+ const phase2 = phases.find((p) => p.id === "phase-2-gpai");
25
+ expect(phase2).toBeDefined();
26
+ const systemicObligation = phase2?.obligations.find((o) => o.articleRef === "Art. 55");
27
+ expect(systemicObligation).toBeDefined();
28
+ });
29
+
30
+ it("does NOT include high-risk Annex III obligations for GPAI audience", () => {
31
+ const phases = getTimelineForAudiences(["all", "gpai"]);
32
+ const phase3 = phases.find((p) => p.id === "phase-3-high-risk-annex-iii");
33
+ if (phase3) {
34
+ const hasAllAudience = phase3.obligations.some((o) =>
35
+ o.audiences.some((a) => ["all", "gpai"].includes(a))
36
+ );
37
+ expect(hasAllAudience).toBe(true);
38
+ }
39
+ });
40
+
41
+ it("minimal risk level does NOT include GPAI obligations (regression)", () => {
42
+ const phases = getTimelineForRiskLevel("minimal");
43
+ const phase2 = phases.find((p) => p.id === "phase-2-gpai");
44
+ if (phase2) {
45
+ const gpaiOnly = phase2.obligations.filter(
46
+ (o) => o.audiences.includes("gpai") && !o.audiences.includes("all")
47
+ );
48
+ const minimalAudiences = ["all", "minimal"];
49
+ for (const obl of gpaiOnly) {
50
+ const matches = obl.audiences.some((a) => minimalAudiences.includes(a));
51
+ expect(matches).toBe(false);
52
+ }
53
+ }
54
+ });
55
+ });
@@ -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
+ });