@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,232 @@
1
+ import { IMPLEMENTATION_PROVENANCE, isUserFacingPath } from "../../evidence/compliance-evaluation";
2
+ import type {
3
+ ConfidenceLevel,
4
+ DetectedSignal,
5
+ DetectorContext,
6
+ SignalCategory,
7
+ SignalEvidence,
8
+ } from "../../evidence/types";
9
+ import { SOURCE_EXTENSIONS } from "./shared";
10
+
11
+ interface ComplianceRule {
12
+ id: string;
13
+ name: string;
14
+ category: SignalCategory;
15
+ pattern: RegExp;
16
+ confidence: ConfidenceLevel;
17
+ requiresUserFacingPath?: boolean;
18
+ }
19
+
20
+ const rules: ComplianceRule[] = [
21
+ // --- compliance-disclosure ---
22
+ {
23
+ id: "compliance-disclosure-ai-generated",
24
+ name: "AI disclosure text",
25
+ category: "compliance-disclosure",
26
+ pattern: /\b(?:generated\s+by\s+ai|ai[- ]generated|powered\s+by\s+ai)\b/i,
27
+ confidence: "medium",
28
+ },
29
+ {
30
+ id: "compliance-disclosure-ai-notice",
31
+ name: "AI transparency notice",
32
+ category: "compliance-disclosure",
33
+ pattern: /\b(?:artificial\s+intelligence|machine\s+learning\s+model|transparency\s+notice)\b/i,
34
+ confidence: "medium",
35
+ },
36
+
37
+ // --- compliance-logging ---
38
+ {
39
+ id: "compliance-logging-structured",
40
+ name: "Structured logging library",
41
+ category: "compliance-logging",
42
+ pattern:
43
+ /\bfrom\s+['"](?:winston|pino|bunyan)['"]|\brequire\(\s*['"](?:winston|pino|bunyan)['"]\)/,
44
+ confidence: "medium",
45
+ },
46
+ {
47
+ id: "compliance-logging-audit",
48
+ name: "Audit trail pattern",
49
+ category: "compliance-logging",
50
+ pattern: /\b(?:audit[._-]?log|audit[._-]?trail|log[._-]?audit)\b/i,
51
+ confidence: "medium",
52
+ },
53
+ {
54
+ id: "compliance-logging-middleware",
55
+ name: "Logging middleware",
56
+ category: "compliance-logging",
57
+ pattern: /\b(?:logging[Mm]iddleware|requestLogger|morgan)\b/,
58
+ confidence: "low",
59
+ },
60
+
61
+ // --- compliance-oversight ---
62
+ {
63
+ id: "compliance-oversight-review",
64
+ name: "Human review flow",
65
+ category: "compliance-oversight",
66
+ pattern: /\b(?:human[_-]?review|manual[_-]?review|approval[_-]?flow|review[_-]?gate)\b/i,
67
+ confidence: "medium",
68
+ },
69
+ {
70
+ id: "compliance-oversight-approve-reject",
71
+ name: "Approval/rejection mechanism",
72
+ category: "compliance-oversight",
73
+ pattern: /\b(?:onApprove|onReject|handleApproval|approveAction|rejectAction)\b/,
74
+ confidence: "medium",
75
+ },
76
+ {
77
+ id: "compliance-oversight-override",
78
+ name: "Override mechanism",
79
+ category: "compliance-oversight",
80
+ pattern: /\b(?:override[_-]?ai|human[_-]?override|manual[_-]?override)\b/i,
81
+ confidence: "medium",
82
+ },
83
+
84
+ // --- compliance-bias ---
85
+ {
86
+ id: "compliance-bias-fairness",
87
+ name: "Fairness/bias testing",
88
+ category: "compliance-bias",
89
+ pattern: /\b(?:fairness[_-]?test|bias[_-]?test|disparate[_-]?impact|demographic[_-]?parity)\b/i,
90
+ confidence: "medium",
91
+ },
92
+ {
93
+ id: "compliance-bias-benchmark",
94
+ name: "Bias benchmark reference",
95
+ category: "compliance-bias",
96
+ pattern: /\bCrowS[_-]?Pairs\b/i,
97
+ confidence: "medium",
98
+ },
99
+ {
100
+ id: "compliance-bias-euconform",
101
+ name: "EuConform bias utilities",
102
+ category: "compliance-bias",
103
+ pattern: /\bfrom\s+['"]@euconform\/core.*bias/,
104
+ confidence: "medium",
105
+ },
106
+
107
+ // --- compliance-data ---
108
+ {
109
+ id: "compliance-data-quality",
110
+ name: "Data quality checks",
111
+ category: "compliance-data",
112
+ pattern: /\b(?:data[_-]?quality|data[_-]?validation|dataset[_-]?doc|data[_-]?lineage)\b/i,
113
+ confidence: "medium",
114
+ },
115
+ {
116
+ id: "compliance-data-pii",
117
+ name: "PII handling / anonymization",
118
+ category: "compliance-data",
119
+ pattern: /\b(?:pii[_-]?filter|anonymi[sz]e|pseudonymi[sz]e|redact[_-]?pii)\b/i,
120
+ confidence: "medium",
121
+ },
122
+ {
123
+ id: "compliance-data-gdpr",
124
+ name: "GDPR reference",
125
+ category: "compliance-data",
126
+ pattern: /\bGDPR\b|data[_-]?protection[_-]?regulation/i,
127
+ confidence: "low",
128
+ },
129
+
130
+ // --- compliance-reporting ---
131
+ {
132
+ id: "compliance-reporting-json",
133
+ name: "JSON export capability",
134
+ category: "compliance-reporting",
135
+ pattern:
136
+ /\b(?:exportJSON|downloadJSON|writeJSONFile|saveJSONReport|exportAuditJSON|exportAnnexJSON|generateAnnexJson)\b|(?:writeFile(?:Sync)?|saveAs|download)\s*\([^)]*(?:report|audit|annex|compliance)[^)]*\.json/i,
137
+ confidence: "medium",
138
+ },
139
+ {
140
+ id: "compliance-reporting-pdf",
141
+ name: "PDF export capability",
142
+ category: "compliance-reporting",
143
+ pattern:
144
+ /\b(?:jspdf|pdf[_-]?lib|pdfmake|generatePDF|exportPDF|downloadPDF|renderPDF|generateAnnexPdf)\b|puppeteer.*\.pdf/i,
145
+ confidence: "medium",
146
+ },
147
+ {
148
+ id: "compliance-reporting-report",
149
+ name: "Report generation",
150
+ category: "compliance-reporting",
151
+ pattern:
152
+ /\b(?:generateReport|buildReport|exportReport|downloadReport|createReport|annex[_-]?iv[_-]?report|compliance[_-]?report|audit[_-]?report)\b/i,
153
+ confidence: "medium",
154
+ },
155
+
156
+ // --- compliance-incident ---
157
+ {
158
+ id: "compliance-incident-sentry",
159
+ name: "Sentry error reporting",
160
+ category: "compliance-incident",
161
+ pattern: /\bfrom\s+['"]@sentry\/|\bSentry\.init\b|\brequire\(\s*['"]@sentry\//,
162
+ confidence: "medium",
163
+ },
164
+ {
165
+ id: "compliance-incident-boundary",
166
+ name: "Error boundary",
167
+ category: "compliance-incident",
168
+ pattern: /\bErrorBoundary\b|componentDidCatch/,
169
+ confidence: "medium",
170
+ },
171
+ {
172
+ id: "compliance-incident-response",
173
+ name: "Incident response hook",
174
+ category: "compliance-incident",
175
+ pattern: /\b(?:incident[_-]?report|incident[_-]?response|useIncident|alerting[_-]?hook)\b/i,
176
+ confidence: "medium",
177
+ },
178
+ ];
179
+
180
+ function canEvaluateRule(rule: ComplianceRule, file: DetectorContext["file"]): boolean {
181
+ if (!IMPLEMENTATION_PROVENANCE.has(file.provenance)) return false;
182
+ if (rule.category === "compliance-disclosure") return isUserFacingPath(file.relativePath);
183
+ if (rule.requiresUserFacingPath) return isUserFacingPath(file.relativePath);
184
+ return true;
185
+ }
186
+
187
+ function gatherEvidence(
188
+ lines: string[],
189
+ pattern: RegExp,
190
+ filePath: string,
191
+ maxItems: number
192
+ ): SignalEvidence[] {
193
+ const evidence: SignalEvidence[] = [];
194
+ for (let i = 0; i < lines.length; i++) {
195
+ const line = lines[i];
196
+ if (!line) continue;
197
+ if (pattern.test(line)) {
198
+ evidence.push({ file: filePath, line: i + 1, snippet: line.trim().slice(0, 120) });
199
+ if (evidence.length >= maxItems) break;
200
+ }
201
+ }
202
+ return evidence;
203
+ }
204
+
205
+ export function detectCompliance(ctx: DetectorContext): DetectedSignal[] {
206
+ const { file } = ctx;
207
+ if (!SOURCE_EXTENSIONS.has(file.extension)) return [];
208
+
209
+ const signalMap = new Map<string, DetectedSignal>();
210
+
211
+ for (const rule of rules) {
212
+ if (!canEvaluateRule(rule, file)) continue;
213
+
214
+ const evidence = gatherEvidence(ctx.lines, rule.pattern, file.relativePath, 3);
215
+ if (evidence.length === 0) continue;
216
+
217
+ const existing = signalMap.get(rule.id);
218
+ if (existing) {
219
+ existing.evidence.push(...evidence);
220
+ } else {
221
+ signalMap.set(rule.id, {
222
+ id: rule.id,
223
+ name: rule.name,
224
+ category: rule.category,
225
+ confidence: rule.confidence,
226
+ evidence,
227
+ });
228
+ }
229
+ }
230
+
231
+ return Array.from(signalMap.values());
232
+ }
@@ -0,0 +1,316 @@
1
+ import type {
2
+ ConfidenceLevel,
3
+ DetectedSignal,
4
+ DetectorContext,
5
+ SignalCategory,
6
+ SignalEvidence,
7
+ } from "../../evidence/types";
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // .env file detection — variable names only, never values
11
+ // ---------------------------------------------------------------------------
12
+
13
+ interface EnvKeyRule {
14
+ id: string;
15
+ name: string;
16
+ category: SignalCategory;
17
+ pattern: RegExp;
18
+ confidence: ConfidenceLevel;
19
+ }
20
+
21
+ const envKeyRules: EnvKeyRule[] = [
22
+ {
23
+ id: "ai-provider-openai",
24
+ name: "OpenAI API key",
25
+ category: "ai-provider",
26
+ pattern: /^OPENAI_API_KEY\b/,
27
+ confidence: "high",
28
+ },
29
+ {
30
+ id: "ai-provider-anthropic",
31
+ name: "Anthropic API key",
32
+ category: "ai-provider",
33
+ pattern: /^ANTHROPIC_API_KEY\b/,
34
+ confidence: "high",
35
+ },
36
+ {
37
+ id: "ai-provider-azure-openai",
38
+ name: "Azure OpenAI key",
39
+ category: "ai-provider",
40
+ pattern: /^AZURE_OPENAI_/,
41
+ confidence: "high",
42
+ },
43
+ {
44
+ id: "ai-provider-google-ai",
45
+ name: "Google AI key",
46
+ category: "ai-provider",
47
+ pattern: /^(?:GOOGLE_AI_API_KEY|GEMINI_API_KEY)\b/,
48
+ confidence: "high",
49
+ },
50
+ {
51
+ id: "ai-provider-cohere",
52
+ name: "Cohere API key",
53
+ category: "ai-provider",
54
+ pattern: /^COHERE_API_KEY\b/,
55
+ confidence: "high",
56
+ },
57
+ {
58
+ id: "ai-provider-replicate",
59
+ name: "Replicate API token",
60
+ category: "ai-provider",
61
+ pattern: /^REPLICATE_API_TOKEN\b/,
62
+ confidence: "high",
63
+ },
64
+ {
65
+ id: "ai-provider-huggingface",
66
+ name: "HuggingFace token",
67
+ category: "ai-provider",
68
+ pattern: /^HUGGINGFACE_TOKEN\b|^HF_TOKEN\b/,
69
+ confidence: "high",
70
+ },
71
+ {
72
+ id: "local-inference-ollama",
73
+ name: "Ollama host config",
74
+ category: "local-inference",
75
+ pattern: /^OLLAMA_HOST\b|^OLLAMA_BASE_URL\b/,
76
+ confidence: "high",
77
+ },
78
+ {
79
+ id: "rag-pinecone",
80
+ name: "Pinecone API key",
81
+ category: "rag",
82
+ pattern: /^PINECONE_API_KEY\b/,
83
+ confidence: "high",
84
+ },
85
+ {
86
+ id: "rag-weaviate",
87
+ name: "Weaviate config",
88
+ category: "rag",
89
+ pattern: /^WEAVIATE_/,
90
+ confidence: "high",
91
+ },
92
+ {
93
+ id: "rag-chroma",
94
+ name: "ChromaDB config",
95
+ category: "rag",
96
+ pattern: /^CHROMA_/,
97
+ confidence: "high",
98
+ },
99
+ ];
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // Docker Compose detection
103
+ // ---------------------------------------------------------------------------
104
+
105
+ interface DockerServiceRule {
106
+ id: string;
107
+ name: string;
108
+ category: SignalCategory;
109
+ pattern: RegExp;
110
+ confidence: ConfidenceLevel;
111
+ }
112
+
113
+ const dockerServiceRules: DockerServiceRule[] = [
114
+ {
115
+ id: "local-inference-ollama-docker",
116
+ name: "Ollama Docker service",
117
+ category: "local-inference",
118
+ pattern: /\bollama\/ollama\b|image:\s*ollama/,
119
+ confidence: "high",
120
+ },
121
+ {
122
+ id: "rag-chromadb-docker",
123
+ name: "ChromaDB Docker service",
124
+ category: "rag",
125
+ pattern: /\bchromadb\/chroma\b|image:\s*chroma/,
126
+ confidence: "high",
127
+ },
128
+ {
129
+ id: "rag-weaviate-docker",
130
+ name: "Weaviate Docker service",
131
+ category: "rag",
132
+ pattern: /\bsemitechnologies\/weaviate\b|image:\s*weaviate/,
133
+ confidence: "high",
134
+ },
135
+ {
136
+ id: "rag-redis-vector",
137
+ name: "Redis with vector module",
138
+ category: "rag",
139
+ pattern: /redis\/redis-stack|redisearch|redis-stack-server/,
140
+ confidence: "high",
141
+ },
142
+ {
143
+ id: "rag-qdrant-docker",
144
+ name: "Qdrant Docker service",
145
+ category: "rag",
146
+ pattern: /\bqdrant\/qdrant\b|image:\s*qdrant/,
147
+ confidence: "high",
148
+ },
149
+ ];
150
+
151
+ // ---------------------------------------------------------------------------
152
+ // Dockerfile detection
153
+ // ---------------------------------------------------------------------------
154
+
155
+ interface DockerfileRule {
156
+ id: string;
157
+ name: string;
158
+ category: SignalCategory;
159
+ pattern: RegExp;
160
+ confidence: ConfidenceLevel;
161
+ }
162
+
163
+ const dockerfileRules: DockerfileRule[] = [
164
+ {
165
+ id: "local-inference-pip-ai",
166
+ name: "AI pip packages in Dockerfile",
167
+ category: "local-inference",
168
+ pattern:
169
+ /pip\s+install\s+.*\b(?:torch|tensorflow|transformers|langchain|llama[-_]?cpp|vllm|onnxruntime)\b/,
170
+ confidence: "high",
171
+ },
172
+ {
173
+ id: "local-inference-model-download",
174
+ name: "Model download in Dockerfile",
175
+ category: "local-inference",
176
+ pattern:
177
+ /\b(?:huggingface-cli\s+download|wget\s+.*(?:\.gguf|\.safetensors|\.bin)|COPY\s+.*(?:model|weights))\b/i,
178
+ confidence: "high",
179
+ },
180
+ ];
181
+
182
+ // ---------------------------------------------------------------------------
183
+ // File matching helpers
184
+ // ---------------------------------------------------------------------------
185
+
186
+ function isEnvExample(relativePath: string): boolean {
187
+ const basename = relativePath.split("/").pop() ?? "";
188
+ return /^\.env\.(?:example|sample)$/.test(basename);
189
+ }
190
+
191
+ function isDockerCompose(relativePath: string): boolean {
192
+ const basename = relativePath.split("/").pop() ?? "";
193
+ return /^docker-compose/.test(basename);
194
+ }
195
+
196
+ function isDockerfile(relativePath: string): boolean {
197
+ const basename = relativePath.split("/").pop() ?? "";
198
+ return /^Dockerfile/.test(basename);
199
+ }
200
+
201
+ // ---------------------------------------------------------------------------
202
+ // Detector
203
+ // ---------------------------------------------------------------------------
204
+
205
+ export function detectConfig(ctx: DetectorContext): DetectedSignal[] {
206
+ const { file } = ctx;
207
+ const signals: DetectedSignal[] = [];
208
+
209
+ const lines = ctx.lines;
210
+
211
+ if (isEnvExample(file.relativePath)) {
212
+ detectEnvKeys(file.relativePath, lines, signals);
213
+ } else if (isDockerCompose(file.relativePath)) {
214
+ detectDockerServices(file.relativePath, lines, signals);
215
+ } else if (isDockerfile(file.relativePath)) {
216
+ detectDockerfilePatterns(file.relativePath, lines, signals);
217
+ }
218
+
219
+ return signals;
220
+ }
221
+
222
+ function extractEnvVarName(rawLine: string): string | null {
223
+ const line = rawLine.trim();
224
+ if (!line || line.startsWith("#")) return null;
225
+ const eqIndex = line.indexOf("=");
226
+ return eqIndex >= 0 ? line.slice(0, eqIndex).trim() : line.trim();
227
+ }
228
+
229
+ function detectEnvKeys(relativePath: string, lines: string[], signals: DetectedSignal[]): void {
230
+ for (const rule of envKeyRules) {
231
+ const evidence: SignalEvidence[] = [];
232
+
233
+ for (let i = 0; i < lines.length; i++) {
234
+ const varName = extractEnvVarName(lines[i] ?? "");
235
+ if (varName && rule.pattern.test(varName)) {
236
+ evidence.push({ file: relativePath, line: i + 1, snippet: varName });
237
+ }
238
+ }
239
+
240
+ if (evidence.length > 0) {
241
+ signals.push({
242
+ id: rule.id,
243
+ name: rule.name,
244
+ category: rule.category,
245
+ confidence: rule.confidence,
246
+ evidence,
247
+ });
248
+ }
249
+ }
250
+ }
251
+
252
+ function detectDockerServices(
253
+ relativePath: string,
254
+ lines: string[],
255
+ signals: DetectedSignal[]
256
+ ): void {
257
+ for (const rule of dockerServiceRules) {
258
+ const evidence: SignalEvidence[] = [];
259
+
260
+ for (let i = 0; i < lines.length; i++) {
261
+ const line = lines[i];
262
+ if (!line) continue;
263
+ if (rule.pattern.test(line)) {
264
+ evidence.push({
265
+ file: relativePath,
266
+ line: i + 1,
267
+ snippet: line.trim().slice(0, 120),
268
+ });
269
+ if (evidence.length >= 3) break;
270
+ }
271
+ }
272
+
273
+ if (evidence.length > 0) {
274
+ signals.push({
275
+ id: rule.id,
276
+ name: rule.name,
277
+ category: rule.category,
278
+ confidence: rule.confidence,
279
+ evidence,
280
+ });
281
+ }
282
+ }
283
+ }
284
+
285
+ function detectDockerfilePatterns(
286
+ relativePath: string,
287
+ lines: string[],
288
+ signals: DetectedSignal[]
289
+ ): void {
290
+ for (const rule of dockerfileRules) {
291
+ const evidence: SignalEvidence[] = [];
292
+
293
+ for (let i = 0; i < lines.length; i++) {
294
+ const line = lines[i];
295
+ if (!line) continue;
296
+ if (rule.pattern.test(line)) {
297
+ evidence.push({
298
+ file: relativePath,
299
+ line: i + 1,
300
+ snippet: line.trim().slice(0, 120),
301
+ });
302
+ if (evidence.length >= 3) break;
303
+ }
304
+ }
305
+
306
+ if (evidence.length > 0) {
307
+ signals.push({
308
+ id: rule.id,
309
+ name: rule.name,
310
+ category: rule.category,
311
+ confidence: rule.confidence,
312
+ evidence,
313
+ });
314
+ }
315
+ }
316
+ }
@@ -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
+ }