@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.
Files changed (134) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +18 -0
  3. package/assets/datasets/crows-pairs-de.json +613 -0
  4. package/assets/datasets/crows-pairs-en.json +9055 -0
  5. package/dist/datasets/crows-pairs.d.ts +7 -0
  6. package/dist/datasets/crows-pairs.d.ts.map +1 -0
  7. package/dist/datasets/crows-pairs.js +41 -0
  8. package/dist/evidence/bundle.d.ts +23 -0
  9. package/dist/evidence/bundle.d.ts.map +1 -0
  10. package/dist/evidence/bundle.js +44 -0
  11. package/dist/evidence/compliance-evaluation.d.ts +12 -0
  12. package/dist/evidence/compliance-evaluation.d.ts.map +1 -0
  13. package/dist/evidence/compliance-evaluation.js +78 -0
  14. package/dist/evidence/hash.d.ts +2 -0
  15. package/dist/evidence/hash.d.ts.map +1 -0
  16. package/dist/evidence/hash.js +4 -0
  17. package/dist/evidence/index.d.ts +13 -0
  18. package/dist/evidence/index.d.ts.map +1 -0
  19. package/dist/evidence/index.js +11 -0
  20. package/dist/evidence/markdown.d.ts +9 -0
  21. package/dist/evidence/markdown.d.ts.map +1 -0
  22. package/dist/evidence/markdown.js +169 -0
  23. package/dist/evidence/output.d.ts +13 -0
  24. package/dist/evidence/output.d.ts.map +1 -0
  25. package/dist/evidence/output.js +547 -0
  26. package/dist/evidence/types.d.ts +230 -0
  27. package/dist/evidence/types.d.ts.map +1 -0
  28. package/dist/evidence/types.js +7 -0
  29. package/dist/evidence/validate.d.ts +8 -0
  30. package/dist/evidence/validate.d.ts.map +1 -0
  31. package/dist/evidence/validate.js +192 -0
  32. package/dist/index.d.ts +1 -1
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/inference/index.d.ts +1 -1
  35. package/dist/inference/index.d.ts.map +1 -1
  36. package/dist/inference/index.js +1 -1
  37. package/dist/inference/ollama-client.d.ts +11 -3
  38. package/dist/inference/ollama-client.d.ts.map +1 -1
  39. package/dist/inference/ollama-client.js +84 -84
  40. package/dist/scanner/aggregator.d.ts +7 -0
  41. package/dist/scanner/aggregator.d.ts.map +1 -0
  42. package/dist/scanner/aggregator.js +200 -0
  43. package/dist/scanner/detectors/ai-framework-detector.d.ts +3 -0
  44. package/dist/scanner/detectors/ai-framework-detector.d.ts.map +1 -0
  45. package/dist/scanner/detectors/ai-framework-detector.js +74 -0
  46. package/dist/scanner/detectors/ai-provider-detector.d.ts +3 -0
  47. package/dist/scanner/detectors/ai-provider-detector.d.ts.map +1 -0
  48. package/dist/scanner/detectors/ai-provider-detector.js +100 -0
  49. package/dist/scanner/detectors/compliance-detector.d.ts +3 -0
  50. package/dist/scanner/detectors/compliance-detector.d.ts.map +1 -0
  51. package/dist/scanner/detectors/compliance-detector.js +201 -0
  52. package/dist/scanner/detectors/config-detector.d.ts +3 -0
  53. package/dist/scanner/detectors/config-detector.d.ts.map +1 -0
  54. package/dist/scanner/detectors/config-detector.js +248 -0
  55. package/dist/scanner/detectors/framework-detector.d.ts +3 -0
  56. package/dist/scanner/detectors/framework-detector.d.ts.map +1 -0
  57. package/dist/scanner/detectors/framework-detector.js +141 -0
  58. package/dist/scanner/detectors/index.d.ts +8 -0
  59. package/dist/scanner/detectors/index.d.ts.map +1 -0
  60. package/dist/scanner/detectors/index.js +37 -0
  61. package/dist/scanner/detectors/local-inference-detector.d.ts +3 -0
  62. package/dist/scanner/detectors/local-inference-detector.d.ts.map +1 -0
  63. package/dist/scanner/detectors/local-inference-detector.js +93 -0
  64. package/dist/scanner/detectors/markdown-detector.d.ts +3 -0
  65. package/dist/scanner/detectors/markdown-detector.d.ts.map +1 -0
  66. package/dist/scanner/detectors/markdown-detector.js +139 -0
  67. package/dist/scanner/detectors/package-json-detector.d.ts +3 -0
  68. package/dist/scanner/detectors/package-json-detector.d.ts.map +1 -0
  69. package/dist/scanner/detectors/package-json-detector.js +166 -0
  70. package/dist/scanner/detectors/rag-detector.d.ts +3 -0
  71. package/dist/scanner/detectors/rag-detector.d.ts.map +1 -0
  72. package/dist/scanner/detectors/rag-detector.js +132 -0
  73. package/dist/scanner/detectors/shared.d.ts +18 -0
  74. package/dist/scanner/detectors/shared.d.ts.map +1 -0
  75. package/dist/scanner/detectors/shared.js +36 -0
  76. package/dist/scanner/discovery.d.ts +14 -0
  77. package/dist/scanner/discovery.d.ts.map +1 -0
  78. package/dist/scanner/discovery.js +235 -0
  79. package/dist/scanner/index.d.ts +9 -0
  80. package/dist/scanner/index.d.ts.map +1 -0
  81. package/dist/scanner/index.js +8 -0
  82. package/package.json +8 -3
  83. package/src/datasets/crows-pairs.ts +64 -0
  84. package/src/evidence/bundle.ts +65 -0
  85. package/src/evidence/compliance-evaluation.ts +114 -0
  86. package/src/evidence/hash.ts +5 -0
  87. package/src/evidence/index.ts +51 -0
  88. package/src/evidence/markdown.ts +226 -0
  89. package/src/evidence/output.ts +717 -0
  90. package/src/evidence/types.ts +329 -0
  91. package/src/evidence/validate.ts +300 -0
  92. package/src/index.ts +1 -0
  93. package/src/inference/index.ts +6 -1
  94. package/src/inference/ollama-client.ts +111 -104
  95. package/src/scanner/aggregator.ts +258 -0
  96. package/src/scanner/detectors/ai-framework-detector.ts +94 -0
  97. package/src/scanner/detectors/ai-provider-detector.ts +135 -0
  98. package/src/scanner/detectors/compliance-detector.ts +232 -0
  99. package/src/scanner/detectors/config-detector.ts +316 -0
  100. package/src/scanner/detectors/framework-detector.ts +163 -0
  101. package/src/scanner/detectors/index.ts +40 -0
  102. package/src/scanner/detectors/local-inference-detector.ts +124 -0
  103. package/src/scanner/detectors/markdown-detector.ts +176 -0
  104. package/src/scanner/detectors/package-json-detector.ts +202 -0
  105. package/src/scanner/detectors/rag-detector.ts +162 -0
  106. package/src/scanner/detectors/shared.ts +45 -0
  107. package/src/scanner/discovery.ts +290 -0
  108. package/src/scanner/index.ts +9 -0
  109. package/tests/evidence/bundle.test.ts +128 -0
  110. package/tests/evidence/hash.test.ts +25 -0
  111. package/tests/evidence/markdown.test.ts +92 -0
  112. package/tests/evidence/output.test.ts +203 -0
  113. package/tests/evidence/spec-schemas.test.ts +316 -0
  114. package/tests/fixtures/compliance-good/README.md +14 -0
  115. package/tests/fixtures/compliance-good/lib/ai-service.ts +17 -0
  116. package/tests/fixtures/compliance-good/lib/review-flow.ts +11 -0
  117. package/tests/fixtures/compliance-good/package.json +10 -0
  118. package/tests/fixtures/compliance-good/tests/bias-check.test.ts +13 -0
  119. package/tests/fixtures/nextjs-openai/README.md +5 -0
  120. package/tests/fixtures/nextjs-openai/app/page.tsx +11 -0
  121. package/tests/fixtures/nextjs-openai/package.json +9 -0
  122. package/tests/fixtures/ollama-rag/.env.example +2 -0
  123. package/tests/fixtures/ollama-rag/docker-compose.yml +9 -0
  124. package/tests/fixtures/ollama-rag/package.json +9 -0
  125. package/tests/fixtures/ollama-rag/server.ts +21 -0
  126. package/tests/fixtures/plain-webapp/README.md +3 -0
  127. package/tests/fixtures/plain-webapp/index.js +5 -0
  128. package/tests/fixtures/plain-webapp/package.json +8 -0
  129. package/tests/gpai-classifier.test.ts +4 -4
  130. package/tests/gpai-timeline.test.ts +2 -2
  131. package/tests/scanner/aggregator.test.ts +122 -0
  132. package/tests/scanner/detectors.test.ts +274 -0
  133. package/tests/scanner/discovery.test.ts +87 -0
  134. package/vitest.config.ts +1 -0
@@ -0,0 +1,163 @@
1
+ import type {
2
+ ConfidenceLevel,
3
+ DetectedSignal,
4
+ DetectorContext,
5
+ SignalEvidence,
6
+ } from "../../evidence/types";
7
+ import { EXTENDED_EXTENSIONS } from "./shared";
8
+
9
+ interface FrameworkRule {
10
+ name: string;
11
+ category: "framework" | "runtime";
12
+ /** Match against file name (basename) */
13
+ configPattern?: RegExp;
14
+ /** Match against file content (line-level) */
15
+ importPattern?: RegExp;
16
+ configConfidence: ConfidenceLevel;
17
+ importConfidence: ConfidenceLevel;
18
+ }
19
+
20
+ const rules: FrameworkRule[] = [
21
+ // Frameworks
22
+ {
23
+ name: "nextjs",
24
+ category: "framework",
25
+ configPattern: /^next\.config\./,
26
+ importPattern: /\bfrom\s+['"]next[\/'"]|\brequire\(\s*['"]next['"]\)/,
27
+ configConfidence: "high",
28
+ importConfidence: "medium",
29
+ },
30
+ {
31
+ name: "react",
32
+ category: "framework",
33
+ importPattern: /\bfrom\s+['"]react['"]|\brequire\(\s*['"]react['"]\)/,
34
+ configConfidence: "high",
35
+ importConfidence: "medium",
36
+ },
37
+ {
38
+ name: "express",
39
+ category: "framework",
40
+ importPattern: /\bfrom\s+['"]express['"]|\brequire\(\s*['"]express['"]\)/,
41
+ configConfidence: "high",
42
+ importConfidence: "medium",
43
+ },
44
+ {
45
+ name: "fastify",
46
+ category: "framework",
47
+ importPattern: /\bfrom\s+['"]fastify['"]|\brequire\(\s*['"]fastify['"]\)/,
48
+ configConfidence: "high",
49
+ importConfidence: "medium",
50
+ },
51
+ {
52
+ name: "nestjs",
53
+ category: "framework",
54
+ importPattern: /\bfrom\s+['"]@nestjs\/|\brequire\(\s*['"]@nestjs\//,
55
+ configConfidence: "high",
56
+ importConfidence: "medium",
57
+ },
58
+ {
59
+ name: "hono",
60
+ category: "framework",
61
+ importPattern: /\bfrom\s+['"]hono['"]|\brequire\(\s*['"]hono['"]\)/,
62
+ configConfidence: "high",
63
+ importConfidence: "medium",
64
+ },
65
+ {
66
+ name: "elysia",
67
+ category: "framework",
68
+ importPattern: /\bfrom\s+['"]elysia['"]|\brequire\(\s*['"]elysia['"]\)/,
69
+ configConfidence: "high",
70
+ importConfidence: "medium",
71
+ },
72
+ {
73
+ name: "vite",
74
+ category: "framework",
75
+ configPattern: /^vite\.config\./,
76
+ importPattern: /\bfrom\s+['"]vite['"]|\brequire\(\s*['"]vite['"]\)/,
77
+ configConfidence: "high",
78
+ importConfidence: "medium",
79
+ },
80
+ // Runtimes
81
+ {
82
+ name: "nodejs",
83
+ category: "runtime",
84
+ importPattern:
85
+ /\bfrom\s+['"]node:(?:fs|http|path|crypto|stream)['"]|\bfrom\s+['"](?:fs|http|net|dgram)['"]|\brequire\(\s*['"](?:fs|http|path|crypto)['"]\)/,
86
+ configConfidence: "high",
87
+ importConfidence: "medium",
88
+ },
89
+ {
90
+ name: "bun",
91
+ category: "runtime",
92
+ importPattern: /\bBun\.serve\b|\bBun\.file\b|\bBun\.write\b/,
93
+ configConfidence: "high",
94
+ importConfidence: "medium",
95
+ },
96
+ {
97
+ name: "deno",
98
+ category: "runtime",
99
+ importPattern: /\bDeno\.serve\b|\bDeno\.readFile\b|\bDeno\.env\b/,
100
+ configConfidence: "high",
101
+ importConfidence: "medium",
102
+ },
103
+ ];
104
+
105
+ const CONFIG_NAME_PATTERN = /^(next|vite)\.config\./;
106
+
107
+ function shouldProcess(file: { extension: string; relativePath: string }): boolean {
108
+ if (EXTENDED_EXTENSIONS.has(file.extension)) return true;
109
+ const basename = file.relativePath.split("/").pop() ?? "";
110
+ return CONFIG_NAME_PATTERN.test(basename);
111
+ }
112
+
113
+ function findImportEvidence(lines: string[], pattern: RegExp, filePath: string): SignalEvidence[] {
114
+ const evidence: SignalEvidence[] = [];
115
+ for (let i = 0; i < lines.length; i++) {
116
+ const line = lines[i];
117
+ if (!line) continue;
118
+ if (pattern.test(line)) {
119
+ evidence.push({ file: filePath, line: i + 1, snippet: line.trim().slice(0, 120) });
120
+ if (evidence.length >= 3) break;
121
+ }
122
+ }
123
+ return evidence;
124
+ }
125
+
126
+ export function detectFrameworks(ctx: DetectorContext): DetectedSignal[] {
127
+ const { file } = ctx;
128
+ if (!shouldProcess(file)) return [];
129
+
130
+ const basename = file.relativePath.split("/").pop() ?? "";
131
+ const seen = new Set<string>();
132
+ const signals: DetectedSignal[] = [];
133
+
134
+ for (const rule of rules) {
135
+ if (rule.configPattern?.test(basename) && !seen.has(rule.name)) {
136
+ seen.add(rule.name);
137
+ signals.push({
138
+ id: `${rule.category}-${rule.name}`,
139
+ name: rule.name,
140
+ category: rule.category,
141
+ confidence: rule.configConfidence,
142
+ evidence: [
143
+ { file: file.relativePath, line: 1, snippet: `Config file detected: ${basename}` },
144
+ ],
145
+ });
146
+ }
147
+
148
+ if (!rule.importPattern) continue;
149
+ const evidence = findImportEvidence(ctx.lines, rule.importPattern, file.relativePath);
150
+ if (evidence.length > 0 && !seen.has(rule.name)) {
151
+ seen.add(rule.name);
152
+ signals.push({
153
+ id: `${rule.category}-${rule.name}`,
154
+ name: rule.name,
155
+ category: rule.category,
156
+ confidence: rule.importConfidence,
157
+ evidence,
158
+ });
159
+ }
160
+ }
161
+
162
+ return signals;
163
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Signal detector registry.
3
+ *
4
+ * Runs all registered detectors against a file and returns combined signals.
5
+ */
6
+
7
+ import type { DetectedSignal, DetectorContext, SignalDetector } from "../../evidence/types";
8
+ import { detectAiFrameworkSignals } from "./ai-framework-detector";
9
+ import { detectAiProviderSignals } from "./ai-provider-detector";
10
+ import { detectCompliance } from "./compliance-detector";
11
+ import { detectConfig } from "./config-detector";
12
+ import { detectFrameworks } from "./framework-detector";
13
+ import { detectLocalInferenceSignals } from "./local-inference-detector";
14
+ import { detectMarkdown } from "./markdown-detector";
15
+ import { detectPackageJsonSignals } from "./package-json-detector";
16
+ import { detectRagSignals } from "./rag-detector";
17
+
18
+ const ALL_DETECTORS: SignalDetector[] = [
19
+ detectPackageJsonSignals,
20
+ detectAiProviderSignals,
21
+ detectAiFrameworkSignals,
22
+ detectLocalInferenceSignals,
23
+ detectRagSignals,
24
+ detectFrameworks,
25
+ detectCompliance,
26
+ detectConfig,
27
+ detectMarkdown,
28
+ ];
29
+
30
+ export function runAllDetectors(ctx: DetectorContext): DetectedSignal[] {
31
+ const signals: DetectedSignal[] = [];
32
+ for (const detector of ALL_DETECTORS) {
33
+ try {
34
+ signals.push(...detector(ctx));
35
+ } catch {
36
+ // Individual detector failure should not break the scan
37
+ }
38
+ }
39
+ return signals;
40
+ }
@@ -0,0 +1,124 @@
1
+ import type {
2
+ ConfidenceLevel,
3
+ DetectedSignal,
4
+ DetectorContext,
5
+ SignalEvidence,
6
+ } from "../../evidence/types";
7
+ import { EXTENDED_EXTENSIONS } from "./shared";
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Detection rules
11
+ // ---------------------------------------------------------------------------
12
+
13
+ interface DetectionRule {
14
+ id: string;
15
+ name: string;
16
+ pattern: RegExp;
17
+ confidence: ConfidenceLevel;
18
+ /** File extensions this rule applies to. Empty = all allowed extensions. */
19
+ extensions?: string[];
20
+ }
21
+
22
+ const RULES: DetectionRule[] = [
23
+ // Ollama API URL
24
+ {
25
+ id: "local-inference-ollama-api",
26
+ name: "Ollama API (localhost:11434)",
27
+ pattern: /localhost:11434/,
28
+ confidence: "high",
29
+ },
30
+ // Ollama imports
31
+ {
32
+ id: "local-inference-ollama-import",
33
+ name: "Ollama SDK import",
34
+ pattern:
35
+ /(?:from\s+['"](?:ollama|@ollama\/ollama)['"]|require\s*\(\s*['"](?:ollama|@ollama\/ollama)['"])/,
36
+ confidence: "medium",
37
+ extensions: [".ts", ".tsx", ".js", ".jsx"],
38
+ },
39
+ // Transformers.js browser usage (pipeline / env)
40
+ {
41
+ id: "local-inference-transformers-browser",
42
+ name: "Transformers.js (browser inference)",
43
+ pattern: /(?:from\s+['"]@xenova\/transformers['"]|\.pipeline\s*\(|env\.allowLocalModels)/,
44
+ confidence: "medium",
45
+ extensions: [".ts", ".tsx", ".js", ".jsx"],
46
+ },
47
+ // ONNX Runtime
48
+ {
49
+ id: "local-inference-onnx",
50
+ name: "ONNX Runtime",
51
+ pattern:
52
+ /(?:from\s+['"]onnxruntime[^'"]*['"]|require\s*\(\s*['"]onnxruntime[^'"]*['"]|onnxruntime)/,
53
+ confidence: "medium",
54
+ },
55
+ // llama.cpp references
56
+ {
57
+ id: "local-inference-llamacpp",
58
+ name: "llama.cpp",
59
+ pattern: /llama\.cpp|llama-cpp|llamacpp/,
60
+ confidence: "medium",
61
+ },
62
+ // Docker Compose ollama service
63
+ {
64
+ id: "local-inference-ollama-docker",
65
+ name: "Ollama Docker service",
66
+ pattern: /ollama\/ollama|image:\s*ollama/,
67
+ confidence: "high",
68
+ extensions: [".yaml", ".yml", ".json"],
69
+ },
70
+ ];
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Detector
74
+ // ---------------------------------------------------------------------------
75
+
76
+ function matchRuleToLine(
77
+ rule: (typeof RULES)[number],
78
+ line: string,
79
+ lineNum: number,
80
+ filePath: string,
81
+ signalMap: Map<string, DetectedSignal>
82
+ ): void {
83
+ if (!rule.pattern.test(line)) return;
84
+
85
+ const evidence: SignalEvidence = {
86
+ file: filePath,
87
+ line: lineNum,
88
+ snippet: line.trim().slice(0, 200),
89
+ };
90
+
91
+ const existing = signalMap.get(rule.id);
92
+ if (existing) {
93
+ existing.evidence.push(evidence);
94
+ if (rule.confidence === "high") existing.confidence = "high";
95
+ } else {
96
+ signalMap.set(rule.id, {
97
+ id: rule.id,
98
+ name: rule.name,
99
+ category: "local-inference",
100
+ confidence: rule.confidence,
101
+ evidence: [evidence],
102
+ });
103
+ }
104
+ }
105
+
106
+ export function detectLocalInferenceSignals(ctx: DetectorContext): DetectedSignal[] {
107
+ const { file } = ctx;
108
+ if (!EXTENDED_EXTENSIONS.has(file.extension)) return [];
109
+
110
+ const lines = ctx.lines;
111
+ const signalMap = new Map<string, DetectedSignal>();
112
+
113
+ for (let i = 0; i < lines.length; i++) {
114
+ const line = lines[i];
115
+ if (!line) continue;
116
+
117
+ for (const rule of RULES) {
118
+ if (rule.extensions && !rule.extensions.includes(file.extension)) continue;
119
+ matchRuleToLine(rule, line, i + 1, file.relativePath, signalMap);
120
+ }
121
+ }
122
+
123
+ return Array.from(signalMap.values());
124
+ }
@@ -0,0 +1,176 @@
1
+ import type {
2
+ ConfidenceLevel,
3
+ DetectedSignal,
4
+ DetectorContext,
5
+ SignalCategory,
6
+ SignalEvidence,
7
+ } from "../../evidence/types";
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Rules
11
+ // ---------------------------------------------------------------------------
12
+
13
+ interface MarkdownRule {
14
+ id: string;
15
+ name: string;
16
+ category: SignalCategory;
17
+ /** Match against individual lines */
18
+ pattern: RegExp;
19
+ confidence: ConfidenceLevel;
20
+ /** When true, only matches inside heading-level sections (## ...) */
21
+ sectionHeading?: boolean;
22
+ }
23
+
24
+ const rules: MarkdownRule[] = [
25
+ // --- compliance-disclosure ---
26
+ {
27
+ id: "compliance-disclosure-readme",
28
+ name: "AI disclosure in README",
29
+ category: "compliance-disclosure",
30
+ pattern:
31
+ /\b(?:artificial\s+intelligence|machine\s+learning|AI[- ]powered|AI[- ]generated|uses?\s+AI|deep\s+learning|neural\s+network|large\s+language\s+model|LLM)\b/i,
32
+ confidence: "low",
33
+ },
34
+ {
35
+ id: "compliance-disclosure-transparency",
36
+ name: "Transparency notice",
37
+ category: "compliance-disclosure",
38
+ pattern: /\b(?:transparency\s+notice|AI\s+disclosure|generated\s+by\s+AI)\b/i,
39
+ confidence: "medium",
40
+ },
41
+
42
+ // --- Model card ---
43
+ {
44
+ id: "compliance-disclosure-model-card",
45
+ name: "Model card documentation",
46
+ category: "compliance-disclosure",
47
+ pattern: /^#{1,3}\s+Model\s+Card\b/i,
48
+ confidence: "medium",
49
+ sectionHeading: true,
50
+ },
51
+
52
+ // --- compliance-data ---
53
+ {
54
+ id: "compliance-data-docs",
55
+ name: "Data governance documentation",
56
+ category: "compliance-data",
57
+ pattern: /^#{1,3}\s+(?:Data\s+(?:Sources?|Governance|Quality|Lineage)|Training\s+Data)\b/i,
58
+ confidence: "medium",
59
+ sectionHeading: true,
60
+ },
61
+ {
62
+ id: "compliance-data-reference",
63
+ name: "Data source references",
64
+ category: "compliance-data",
65
+ pattern: /\b(?:training\s+data|data\s+sources?|dataset\s+documentation)\b/i,
66
+ confidence: "low",
67
+ },
68
+
69
+ // --- Privacy / GDPR ---
70
+ {
71
+ id: "compliance-data-privacy",
72
+ name: "Privacy policy reference",
73
+ category: "compliance-data",
74
+ pattern: /\b(?:privacy\s+policy|GDPR|data\s+protection|DSGVO)\b/i,
75
+ confidence: "medium",
76
+ },
77
+
78
+ // --- License / AI-specific ---
79
+ {
80
+ id: "compliance-disclosure-license",
81
+ name: "AI-relevant license terms",
82
+ category: "compliance-disclosure",
83
+ pattern:
84
+ /\b(?:model\s+license|AI\s+license|responsible\s+AI|acceptable\s+use\s+policy|RAIL\s+license)\b/i,
85
+ confidence: "medium",
86
+ },
87
+ ];
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // File filtering
91
+ // ---------------------------------------------------------------------------
92
+
93
+ /** Files that should always be skipped unless they contain AI-specific content */
94
+ const SKIP_BASENAMES = new Set([
95
+ "changelog.md",
96
+ "changes.md",
97
+ "history.md",
98
+ "contributing.md",
99
+ "code_of_conduct.md",
100
+ ]);
101
+
102
+ /** Only process these docs-level paths */
103
+ function isRelevantMarkdown(relativePath: string): boolean {
104
+ const lower = relativePath.toLowerCase();
105
+ const basename = lower.split("/").pop() ?? "";
106
+
107
+ // Always process README files at any level
108
+ if (basename.startsWith("readme")) return true;
109
+
110
+ // Process files inside docs/ directories
111
+ if (lower.includes("/docs/") || lower.startsWith("docs/")) {
112
+ return !SKIP_BASENAMES.has(basename);
113
+ }
114
+
115
+ // For other markdown files, skip known non-relevant ones
116
+ if (SKIP_BASENAMES.has(basename)) return false;
117
+
118
+ return true;
119
+ }
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // Detector
123
+ // ---------------------------------------------------------------------------
124
+
125
+ function gatherMarkdownEvidence(
126
+ rule: (typeof rules)[number],
127
+ lines: string[],
128
+ filePath: string
129
+ ): SignalEvidence[] {
130
+ const evidence: SignalEvidence[] = [];
131
+ for (let i = 0; i < lines.length; i++) {
132
+ const line = lines[i];
133
+ if (!line) continue;
134
+ if (rule.sectionHeading && !line.startsWith("#")) continue;
135
+ if (rule.pattern.test(line)) {
136
+ evidence.push({ file: filePath, line: i + 1, snippet: line.trim().slice(0, 120) });
137
+ if (evidence.length >= 5) break;
138
+ }
139
+ }
140
+ return evidence;
141
+ }
142
+
143
+ function isSkippableGenericContent(basename: string, evidence: SignalEvidence[]): boolean {
144
+ if (!SKIP_BASENAMES.has(basename)) return false;
145
+ return !evidence.some((e) => /\bAI\b|\bML\b|\bmodel\b|\binference\b/i.test(e.snippet));
146
+ }
147
+
148
+ export function detectMarkdown(ctx: DetectorContext): DetectedSignal[] {
149
+ const { file } = ctx;
150
+ if (file.extension !== ".md") return [];
151
+ if (!isRelevantMarkdown(file.relativePath)) return [];
152
+
153
+ const signalMap = new Map<string, DetectedSignal>();
154
+ const basename = file.relativePath.toLowerCase().split("/").pop() ?? "";
155
+
156
+ for (const rule of rules) {
157
+ const evidence = gatherMarkdownEvidence(rule, ctx.lines, file.relativePath);
158
+ if (evidence.length === 0) continue;
159
+ if (isSkippableGenericContent(basename, evidence)) continue;
160
+
161
+ const existing = signalMap.get(rule.id);
162
+ if (existing) {
163
+ existing.evidence.push(...evidence);
164
+ } else {
165
+ signalMap.set(rule.id, {
166
+ id: rule.id,
167
+ name: rule.name,
168
+ category: rule.category,
169
+ confidence: rule.confidence,
170
+ evidence,
171
+ });
172
+ }
173
+ }
174
+
175
+ return Array.from(signalMap.values());
176
+ }
@@ -0,0 +1,202 @@
1
+ import { basename } from "node:path";
2
+ import type { DetectedSignal, DetectorContext, SignalCategory } from "../../evidence/types";
3
+ import { escapeRegex } from "./shared";
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Known package mappings
7
+ // ---------------------------------------------------------------------------
8
+
9
+ interface PackageInfo {
10
+ category: SignalCategory;
11
+ label: string;
12
+ }
13
+
14
+ const AI_PACKAGES: Record<string, PackageInfo> = {
15
+ // Providers
16
+ openai: { category: "ai-provider", label: "OpenAI SDK" },
17
+ "@anthropic-ai/sdk": { category: "ai-provider", label: "Anthropic SDK" },
18
+ "cohere-ai": { category: "ai-provider", label: "Cohere SDK" },
19
+ "@google/generative-ai": { category: "ai-provider", label: "Google Generative AI" },
20
+ "@mistralai/mistralai": { category: "ai-provider", label: "Mistral AI SDK" },
21
+
22
+ // Frameworks
23
+ langchain: { category: "ai-framework", label: "LangChain" },
24
+ "@langchain/core": { category: "ai-framework", label: "LangChain Core" },
25
+ llamaindex: { category: "ai-framework", label: "LlamaIndex" },
26
+ "@xenova/transformers": { category: "ai-framework", label: "Transformers.js" },
27
+ ai: { category: "ai-framework", label: "Vercel AI SDK" },
28
+ "@huggingface/inference": { category: "ai-framework", label: "HuggingFace Inference" },
29
+
30
+ // Local inference
31
+ ollama: { category: "local-inference", label: "Ollama" },
32
+ "@ollama/ollama": { category: "local-inference", label: "Ollama SDK" },
33
+
34
+ // RAG / Vector
35
+ chromadb: { category: "rag", label: "ChromaDB" },
36
+ "@pinecone-database/pinecone": { category: "rag", label: "Pinecone" },
37
+ "weaviate-client": { category: "rag", label: "Weaviate" },
38
+ "@qdrant/js-client-rest": { category: "rag", label: "Qdrant" },
39
+ pgvector: { category: "rag", label: "pgvector" },
40
+
41
+ // Bias / Eval
42
+ "@euconform/core": { category: "compliance-bias", label: "EuConform Core" },
43
+ };
44
+
45
+ const FRAMEWORK_PACKAGES: Record<string, PackageInfo> = {
46
+ next: { category: "framework", label: "Next.js" },
47
+ react: { category: "framework", label: "React" },
48
+ express: { category: "framework", label: "Express" },
49
+ fastify: { category: "framework", label: "Fastify" },
50
+ "@nestjs/core": { category: "framework", label: "NestJS" },
51
+ hono: { category: "framework", label: "Hono" },
52
+ elysia: { category: "framework", label: "Elysia" },
53
+ };
54
+
55
+ const ALL_PACKAGES: Record<string, PackageInfo> = {
56
+ ...AI_PACKAGES,
57
+ ...FRAMEWORK_PACKAGES,
58
+ };
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Helpers
62
+ // ---------------------------------------------------------------------------
63
+
64
+ const PACKAGE_JSON_NAMES = new Set([
65
+ "package.json",
66
+ "pnpm-lock.yaml",
67
+ "package-lock.json",
68
+ "yarn.lock",
69
+ ]);
70
+
71
+ function isPackageJsonFile(relativePath: string): boolean {
72
+ return PACKAGE_JSON_NAMES.has(basename(relativePath));
73
+ }
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // Detector
77
+ // ---------------------------------------------------------------------------
78
+
79
+ export function detectPackageJsonSignals(ctx: DetectorContext): DetectedSignal[] {
80
+ const { file } = ctx;
81
+ if (!isPackageJsonFile(file.relativePath)) return [];
82
+
83
+ const fileName = basename(file.relativePath);
84
+
85
+ if (fileName === "package.json") {
86
+ return detectFromPackageJson(file.relativePath, file.content);
87
+ }
88
+
89
+ // Lock files — scan for known package name strings
90
+ return detectFromLockFile(file.relativePath, file.content);
91
+ }
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // package.json detection
95
+ // ---------------------------------------------------------------------------
96
+
97
+ function detectFromPackageJson(filePath: string, content: string): DetectedSignal[] {
98
+ let parsed: Record<string, unknown>;
99
+ try {
100
+ parsed = JSON.parse(content) as Record<string, unknown>;
101
+ } catch {
102
+ return [];
103
+ }
104
+
105
+ const signals: DetectedSignal[] = [];
106
+ const deps = {
107
+ ...(parsed.dependencies as Record<string, string> | undefined),
108
+ ...(parsed.devDependencies as Record<string, string> | undefined),
109
+ };
110
+
111
+ for (const [pkg, info] of Object.entries(ALL_PACKAGES)) {
112
+ if (pkg in deps) {
113
+ const version = deps[pkg] ?? "unknown";
114
+ signals.push({
115
+ id: `${info.category}-${pkg.replace(/[@/]/g, "").replace(/\s/g, "-")}`,
116
+ name: info.label,
117
+ category: info.category,
118
+ confidence: "high",
119
+ evidence: [
120
+ {
121
+ file: filePath,
122
+ snippet: `"${pkg}": "${version}"`,
123
+ },
124
+ ],
125
+ });
126
+ }
127
+ }
128
+
129
+ // Monorepo signals
130
+ if (parsed.workspaces) {
131
+ signals.push({
132
+ id: "runtime-monorepo-workspaces",
133
+ name: "Monorepo (workspaces)",
134
+ category: "runtime",
135
+ confidence: "high",
136
+ evidence: [
137
+ {
138
+ file: filePath,
139
+ snippet: `"workspaces": ${JSON.stringify(parsed.workspaces).slice(0, 120)}`,
140
+ },
141
+ ],
142
+ });
143
+ }
144
+
145
+ const devDeps = parsed.devDependencies as Record<string, string> | undefined;
146
+ if (devDeps && "turbo" in devDeps) {
147
+ signals.push({
148
+ id: "runtime-monorepo-turbo",
149
+ name: "Monorepo (Turborepo)",
150
+ category: "runtime",
151
+ confidence: "high",
152
+ evidence: [
153
+ {
154
+ file: filePath,
155
+ snippet: `"turbo": "${devDeps.turbo}"`,
156
+ },
157
+ ],
158
+ });
159
+ }
160
+
161
+ return signals;
162
+ }
163
+
164
+ // ---------------------------------------------------------------------------
165
+ // Lock-file detection (string search, no full parse)
166
+ // ---------------------------------------------------------------------------
167
+
168
+ // Pre-compiled lock-file patterns — avoids RegExp construction on every call
169
+ const LOCK_FILE_PATTERNS: Map<string, RegExp> = new Map(
170
+ Object.keys(AI_PACKAGES).map((pkg) => [pkg, new RegExp(`["'/]${escapeRegex(pkg)}["'/@:]`)])
171
+ );
172
+
173
+ function detectFromLockFile(filePath: string, content: string): DetectedSignal[] {
174
+ const signals: DetectedSignal[] = [];
175
+ const seen = new Set<string>();
176
+
177
+ for (const [pkg, info] of Object.entries(AI_PACKAGES)) {
178
+ if (seen.has(pkg)) continue;
179
+
180
+ // Use boundary-aware pattern to avoid false positives on short names
181
+ // Lock files quote package names or use them as path segments
182
+ const pattern = LOCK_FILE_PATTERNS.get(pkg);
183
+ if (!pattern) continue;
184
+ if (pattern.test(content)) {
185
+ seen.add(pkg);
186
+ signals.push({
187
+ id: `${info.category}-${pkg.replace(/[@/]/g, "").replace(/\s/g, "-")}`,
188
+ name: info.label,
189
+ category: info.category,
190
+ confidence: "medium",
191
+ evidence: [
192
+ {
193
+ file: filePath,
194
+ snippet: `Found "${pkg}" in lock file`,
195
+ },
196
+ ],
197
+ });
198
+ }
199
+ }
200
+
201
+ return signals;
202
+ }