@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,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
|
+
}
|