@euconform/core 1.0.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +12 -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 +175 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/inference/ollama-client.d.ts +3 -0
- package/dist/inference/ollama-client.d.ts.map +1 -1
- package/dist/inference/ollama-client.js +69 -71
- package/dist/legal-checks/annex-iv-report.d.ts +3 -0
- package/dist/legal-checks/annex-iv-report.d.ts.map +1 -1
- package/dist/legal-checks/annex-iv-report.js +1 -0
- package/dist/legal-checks/deadline-timeline.d.ts +57 -0
- package/dist/legal-checks/deadline-timeline.d.ts.map +1 -0
- package/dist/legal-checks/deadline-timeline.js +265 -0
- package/dist/legal-checks/gap-analysis.d.ts +48 -0
- package/dist/legal-checks/gap-analysis.d.ts.map +1 -0
- package/dist/legal-checks/gap-analysis.js +397 -0
- package/dist/legal-checks/gpai-classifier.d.ts +49 -0
- package/dist/legal-checks/gpai-classifier.d.ts.map +1 -0
- package/dist/legal-checks/gpai-classifier.js +197 -0
- package/dist/legal-checks/index.d.ts +6 -0
- package/dist/legal-checks/index.d.ts.map +1 -1
- package/dist/legal-checks/index.js +3 -0
- 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 +6 -3
- 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 +270 -0
- package/src/index.ts +25 -0
- package/src/inference/ollama-client.ts +93 -86
- package/src/legal-checks/annex-iv-report.ts +4 -0
- package/src/legal-checks/deadline-timeline.ts +313 -0
- package/src/legal-checks/gap-analysis.ts +454 -0
- package/src/legal-checks/gpai-classifier.ts +281 -0
- package/src/legal-checks/index.ts +33 -0
- 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/ecef-schemas.test.ts +316 -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/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 +125 -0
- package/tests/gpai-timeline.test.ts +55 -0
- 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
- package/.turbo/turbo-check-types.log +0 -4
- package/.turbo/turbo-test.log +0 -364
- package/coverage/base.css +0 -224
- package/coverage/block-navigation.js +0 -87
- package/coverage/clover.xml +0 -3282
- package/coverage/coverage-final.json +0 -20
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +0 -161
- package/coverage/prettify.css +0 -1
- package/coverage/prettify.js +0 -2
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +0 -210
- package/coverage/src/fairness-metrics.ts.html +0 -1579
- package/coverage/src/index.html +0 -161
- package/coverage/src/index.ts.html +0 -535
- package/coverage/src/inference/browser-inference.ts.html +0 -859
- package/coverage/src/inference/capability-detection.ts.html +0 -2227
- package/coverage/src/inference/index.html +0 -176
- package/coverage/src/inference/index.ts.html +0 -553
- package/coverage/src/inference/inference-factory.ts.html +0 -751
- package/coverage/src/inference/ollama-client.ts.html +0 -1627
- package/coverage/src/legal-checks/annex-iv-report.ts.html +0 -556
- package/coverage/src/legal-checks/bias-metrics.ts.html +0 -1855
- package/coverage/src/legal-checks/data-governance.ts.html +0 -529
- package/coverage/src/legal-checks/human-oversight-logging.ts.html +0 -490
- package/coverage/src/legal-checks/index.html +0 -206
- package/coverage/src/legal-checks/index.ts.html +0 -250
- package/coverage/src/legal-checks/risk-classifier.ts.html +0 -940
- package/coverage/src/legal-checks/robustness-transparency.ts.html +0 -571
- package/coverage/src/metrics/crows-pairs-bias.ts.html +0 -820
- package/coverage/src/metrics/index.html +0 -146
- package/coverage/src/metrics/index.ts.html +0 -151
- package/coverage/src/metrics/stereotype-bias.ts.html +0 -886
- package/coverage/src/risk-engine.ts.html +0 -1660
- package/coverage/src/types.ts.html +0 -547
- package/dist/legal-checks/disclaimer.d.ts +0 -2
- package/dist/legal-checks/disclaimer.d.ts.map +0 -1
- package/dist/legal-checks/disclaimer.js +0 -1
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EuConform Evidence Engine — Output Generation
|
|
3
|
+
*
|
|
4
|
+
* Transforms a ScanResult into a ScanOutput containing:
|
|
5
|
+
* - ScanReport (euconform.report.v1)
|
|
6
|
+
* - AiBillOfMaterials (euconform.aibom.v1)
|
|
7
|
+
* - summaryMarkdown
|
|
8
|
+
*
|
|
9
|
+
* Pure function — no side effects, no filesystem operations.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { evaluateSignalsForCategories } from "./compliance-evaluation";
|
|
13
|
+
import { generateSummaryMarkdown } from "./markdown";
|
|
14
|
+
import type {
|
|
15
|
+
AiBillOfMaterials,
|
|
16
|
+
AssessmentHint,
|
|
17
|
+
BomComponent,
|
|
18
|
+
BomComponentKind,
|
|
19
|
+
ComplianceSignalGroup,
|
|
20
|
+
ConfidenceLevel,
|
|
21
|
+
DetectedSignal,
|
|
22
|
+
ScanGap,
|
|
23
|
+
ScanOutput,
|
|
24
|
+
ScanReport,
|
|
25
|
+
ScanResult,
|
|
26
|
+
SignalCategory,
|
|
27
|
+
SignalEvidence,
|
|
28
|
+
} from "./types";
|
|
29
|
+
|
|
30
|
+
const COMPLIANCE_AREA_MAP = {
|
|
31
|
+
disclosure: ["compliance-disclosure"] as SignalCategory[],
|
|
32
|
+
biasTesting: ["compliance-bias"] as SignalCategory[],
|
|
33
|
+
reportingExports: ["compliance-reporting"] as SignalCategory[],
|
|
34
|
+
loggingMonitoring: ["compliance-logging"] as SignalCategory[],
|
|
35
|
+
humanOversight: ["compliance-oversight"] as SignalCategory[],
|
|
36
|
+
dataGovernance: ["compliance-data"] as SignalCategory[],
|
|
37
|
+
incidentReporting: ["compliance-incident"] as SignalCategory[],
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const COMPLIANCE_AREA_LABELS: Record<keyof typeof COMPLIANCE_AREA_MAP, string> = {
|
|
41
|
+
disclosure: "AI disclosure",
|
|
42
|
+
biasTesting: "bias testing",
|
|
43
|
+
reportingExports: "reporting and exports",
|
|
44
|
+
loggingMonitoring: "logging and monitoring",
|
|
45
|
+
humanOversight: "human oversight",
|
|
46
|
+
dataGovernance: "data governance",
|
|
47
|
+
incidentReporting: "incident reporting",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const AI_CATEGORIES = new Set<SignalCategory>([
|
|
51
|
+
"ai-provider",
|
|
52
|
+
"ai-framework",
|
|
53
|
+
"ai-model",
|
|
54
|
+
"local-inference",
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
const BOM_CATEGORIES = new Set<SignalCategory>([
|
|
58
|
+
"ai-provider",
|
|
59
|
+
"ai-framework",
|
|
60
|
+
"ai-model",
|
|
61
|
+
"local-inference",
|
|
62
|
+
"rag",
|
|
63
|
+
"training",
|
|
64
|
+
"framework",
|
|
65
|
+
"runtime",
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
const CATEGORY_TO_BOM_KIND: Partial<Record<SignalCategory, BomComponentKind>> = {
|
|
69
|
+
"ai-provider": "inference-provider",
|
|
70
|
+
"ai-framework": "ai-framework",
|
|
71
|
+
"ai-model": "model",
|
|
72
|
+
"local-inference": "inference-provider",
|
|
73
|
+
framework: "framework",
|
|
74
|
+
runtime: "runtime",
|
|
75
|
+
rag: "vector-store",
|
|
76
|
+
training: "dataset",
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const SOURCE_PRIORITY: Record<BomComponent["source"], number> = {
|
|
80
|
+
"package.json": 5,
|
|
81
|
+
config: 4,
|
|
82
|
+
code: 3,
|
|
83
|
+
"lock-file": 2,
|
|
84
|
+
docs: 1,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const EVIDENCE_PROVENANCE_PRIORITY: Record<NonNullable<SignalEvidence["provenance"]>, number> = {
|
|
88
|
+
runtime: 5,
|
|
89
|
+
config: 4,
|
|
90
|
+
tooling: 3,
|
|
91
|
+
"root-docs": 2,
|
|
92
|
+
docs: 1,
|
|
93
|
+
test: 0,
|
|
94
|
+
fixture: 0,
|
|
95
|
+
example: 0,
|
|
96
|
+
generated: 0,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
interface ComplianceAreaEvaluation {
|
|
100
|
+
group: ComplianceSignalGroup;
|
|
101
|
+
implementationSignals: DetectedSignal[];
|
|
102
|
+
supportingSignals: DetectedSignal[];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function hasAI(signals: DetectedSignal[]): boolean {
|
|
106
|
+
return signals.some((signal) => AI_CATEGORIES.has(signal.category));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function highestConfidence(signals: DetectedSignal[]): ConfidenceLevel {
|
|
110
|
+
if (signals.some((signal) => signal.confidence === "high")) return "high";
|
|
111
|
+
if (signals.some((signal) => signal.confidence === "medium")) return "medium";
|
|
112
|
+
return "low";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function collectEvidence(signals: DetectedSignal[]): SignalEvidence[] {
|
|
116
|
+
return normalizeEvidence(signals.flatMap((signal) => signal.evidence));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function uniquePush(items: string[], value: string): void {
|
|
120
|
+
if (!items.includes(value)) items.push(value);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function toKebabCase(value: string): string {
|
|
124
|
+
return value
|
|
125
|
+
.toLowerCase()
|
|
126
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
127
|
+
.replace(/^-+|-+$/g, "");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function makeBomComponentId(kind: BomComponentKind, name: string): string {
|
|
131
|
+
const slug = toKebabCase(name);
|
|
132
|
+
return `${kind}:${slug || "component"}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function compareEvidence(left: SignalEvidence, right: SignalEvidence): number {
|
|
136
|
+
const leftPriority = left.provenance ? EVIDENCE_PROVENANCE_PRIORITY[left.provenance] : 0;
|
|
137
|
+
const rightPriority = right.provenance ? EVIDENCE_PROVENANCE_PRIORITY[right.provenance] : 0;
|
|
138
|
+
|
|
139
|
+
if (leftPriority !== rightPriority) {
|
|
140
|
+
return rightPriority - leftPriority;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (left.file !== right.file) {
|
|
144
|
+
return left.file.localeCompare(right.file);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return (left.line ?? 0) - (right.line ?? 0);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function normalizeEvidence(evidence: SignalEvidence[], maxItems = 12): SignalEvidence[] {
|
|
151
|
+
const unique = new Map<string, SignalEvidence>();
|
|
152
|
+
|
|
153
|
+
for (const item of evidence) {
|
|
154
|
+
const key = `${item.file}:${item.line ?? 0}:${item.snippet}`;
|
|
155
|
+
if (!unique.has(key)) {
|
|
156
|
+
unique.set(key, item);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return [...unique.values()].sort(compareEvidence).slice(0, maxItems);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const SIGNAL_NAME_MAP: [string, string][] = [
|
|
164
|
+
["react", "React"],
|
|
165
|
+
["express", "Express"],
|
|
166
|
+
["fastify", "Fastify"],
|
|
167
|
+
["nestjs", "NestJS"],
|
|
168
|
+
["nodejs", "Node.js"],
|
|
169
|
+
["langchain", "LangChain"],
|
|
170
|
+
["llamaindex", "LlamaIndex"],
|
|
171
|
+
["transformers", "Transformers.js"],
|
|
172
|
+
["anthropic", "Anthropic"],
|
|
173
|
+
["azure", "Azure OpenAI"],
|
|
174
|
+
["ollama", "Ollama"],
|
|
175
|
+
["onnx", "ONNX Runtime"],
|
|
176
|
+
["chroma", "ChromaDB"],
|
|
177
|
+
["qdrant", "Qdrant"],
|
|
178
|
+
["pinecone", "Pinecone"],
|
|
179
|
+
["weaviate", "Weaviate"],
|
|
180
|
+
["pgvector", "pgvector"],
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
function canonicalizeSignalName(signal: DetectedSignal): string {
|
|
184
|
+
const id = signal.id.toLowerCase();
|
|
185
|
+
const lowerName = signal.name.toLowerCase();
|
|
186
|
+
|
|
187
|
+
// Special cases that need extra logic
|
|
188
|
+
if (id.includes("next") || lowerName === "nextjs") return "Next.js";
|
|
189
|
+
if (id.includes("openai")) return signal.category === "ai-provider" ? "OpenAI" : "OpenAI SDK";
|
|
190
|
+
if (id.includes("google") || id.includes("gemini")) return "Google AI";
|
|
191
|
+
if (id.includes("llamacpp") || lowerName.includes("llama.cpp")) return "llama.cpp";
|
|
192
|
+
if (id.includes("turborepo") || lowerName.includes("turborepo")) return "Monorepo (Turborepo)";
|
|
193
|
+
|
|
194
|
+
for (const [key, canonical] of SIGNAL_NAME_MAP) {
|
|
195
|
+
if (id.includes(key)) return canonical;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return signal.name;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const LOCAL_INFERENCE_MAP: [string, string][] = [
|
|
202
|
+
["ollama", "local-ollama"],
|
|
203
|
+
["transformers", "browser-transformers"],
|
|
204
|
+
["onnx", "local-onnx"],
|
|
205
|
+
["llama", "local-llama"],
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
const CLOUD_PROVIDER_MAP: [string, string][] = [
|
|
209
|
+
["openai", "cloud-openai"],
|
|
210
|
+
["anthropic", "cloud-anthropic"],
|
|
211
|
+
["google", "cloud-google"],
|
|
212
|
+
["gemini", "cloud-google"],
|
|
213
|
+
["azure", "cloud-azure"],
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
function lookupInferenceMode(
|
|
217
|
+
id: string,
|
|
218
|
+
entries: [string, string][],
|
|
219
|
+
prefix: string,
|
|
220
|
+
name: string
|
|
221
|
+
): string | null {
|
|
222
|
+
for (const [key, mode] of entries) {
|
|
223
|
+
if (id.includes(key)) return mode;
|
|
224
|
+
}
|
|
225
|
+
const slug = toKebabCase(name);
|
|
226
|
+
return slug ? `${prefix}-${slug}` : null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function canonicalizeInferenceMode(signal: DetectedSignal): string | null {
|
|
230
|
+
const id = signal.id.toLowerCase();
|
|
231
|
+
const name = canonicalizeSignalName(signal);
|
|
232
|
+
|
|
233
|
+
if (signal.category === "local-inference") {
|
|
234
|
+
return lookupInferenceMode(id, LOCAL_INFERENCE_MAP, "local", name);
|
|
235
|
+
}
|
|
236
|
+
if (signal.category === "ai-provider") {
|
|
237
|
+
return lookupInferenceMode(id, CLOUD_PROVIDER_MAP, "cloud", name);
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function deriveSource(evidence: SignalEvidence[]): BomComponent["source"] {
|
|
243
|
+
let strongest: BomComponent["source"] = "code";
|
|
244
|
+
|
|
245
|
+
for (const item of evidence) {
|
|
246
|
+
let source: BomComponent["source"] = "code";
|
|
247
|
+
|
|
248
|
+
if (item.file.endsWith("package.json")) source = "package.json";
|
|
249
|
+
else if (
|
|
250
|
+
item.file.endsWith("pnpm-lock.yaml") ||
|
|
251
|
+
item.file.endsWith("package-lock.json") ||
|
|
252
|
+
item.file.endsWith("yarn.lock")
|
|
253
|
+
) {
|
|
254
|
+
source = "lock-file";
|
|
255
|
+
} else if (item.provenance === "config") {
|
|
256
|
+
source = "config";
|
|
257
|
+
} else if (item.provenance === "root-docs" || item.provenance === "docs") {
|
|
258
|
+
source = "docs";
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (SOURCE_PRIORITY[source] > SOURCE_PRIORITY[strongest]) {
|
|
262
|
+
strongest = source;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return strongest;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function buildAiFootprint(signals: DetectedSignal[]): ScanReport["aiFootprint"] {
|
|
270
|
+
const usesAI = hasAI(signals);
|
|
271
|
+
const inferenceModes: string[] = [];
|
|
272
|
+
const providerHints: string[] = [];
|
|
273
|
+
const ragHints: string[] = [];
|
|
274
|
+
|
|
275
|
+
for (const signal of signals) {
|
|
276
|
+
const mode = canonicalizeInferenceMode(signal);
|
|
277
|
+
if (mode) uniquePush(inferenceModes, mode);
|
|
278
|
+
|
|
279
|
+
if (signal.category === "ai-provider") {
|
|
280
|
+
uniquePush(providerHints, toKebabCase(canonicalizeSignalName(signal)));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (signal.category === "rag" && signal.confidence !== "low") {
|
|
284
|
+
uniquePush(ragHints, canonicalizeSignalName(signal));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return { usesAI, inferenceModes, providerHints, ragHints };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function buildComplianceSignalGroup(
|
|
292
|
+
implementationSignals: DetectedSignal[],
|
|
293
|
+
aiDetected: boolean
|
|
294
|
+
): ComplianceSignalGroup {
|
|
295
|
+
if (implementationSignals.length === 0) {
|
|
296
|
+
return {
|
|
297
|
+
status: aiDetected ? "absent" : "unknown",
|
|
298
|
+
confidence: "low",
|
|
299
|
+
evidence: [],
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const confidence = highestConfidence(implementationSignals);
|
|
304
|
+
return {
|
|
305
|
+
status: confidence === "high" ? "present" : "partial",
|
|
306
|
+
confidence,
|
|
307
|
+
evidence: collectEvidence(implementationSignals),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function evaluateComplianceAreas(
|
|
312
|
+
signals: DetectedSignal[],
|
|
313
|
+
aiDetected: boolean
|
|
314
|
+
): Record<keyof typeof COMPLIANCE_AREA_MAP, ComplianceAreaEvaluation> {
|
|
315
|
+
const result = {} as Record<keyof typeof COMPLIANCE_AREA_MAP, ComplianceAreaEvaluation>;
|
|
316
|
+
|
|
317
|
+
for (const key of Object.keys(COMPLIANCE_AREA_MAP) as (keyof typeof COMPLIANCE_AREA_MAP)[]) {
|
|
318
|
+
const { implementationSignals, supportingSignals } = evaluateSignalsForCategories(
|
|
319
|
+
signals,
|
|
320
|
+
COMPLIANCE_AREA_MAP[key]
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
result[key] = {
|
|
324
|
+
group: buildComplianceSignalGroup(implementationSignals, aiDetected),
|
|
325
|
+
implementationSignals,
|
|
326
|
+
supportingSignals,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return result;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function buildAssessmentHints(
|
|
334
|
+
signals: DetectedSignal[],
|
|
335
|
+
complianceAreas: Record<keyof typeof COMPLIANCE_AREA_MAP, ComplianceAreaEvaluation>,
|
|
336
|
+
openQuestions: ScanResult["openQuestions"]
|
|
337
|
+
): ScanReport["assessmentHints"] {
|
|
338
|
+
const possibleModes: string[] = [];
|
|
339
|
+
const riskIndicators: AssessmentHint[] = [];
|
|
340
|
+
const gpaiIndicators: AssessmentHint[] = [];
|
|
341
|
+
|
|
342
|
+
const aiDetected = hasAI(signals);
|
|
343
|
+
if (!aiDetected) {
|
|
344
|
+
return {
|
|
345
|
+
possibleModes: [],
|
|
346
|
+
riskIndicators: [],
|
|
347
|
+
gpaiIndicators: [],
|
|
348
|
+
openQuestions: openQuestions.map((question) => question.question),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const hasCloudProvider = signals.some((signal) => signal.category === "ai-provider");
|
|
353
|
+
const hasLocalInference = signals.some((signal) => signal.category === "local-inference");
|
|
354
|
+
|
|
355
|
+
if (hasCloudProvider) {
|
|
356
|
+
possibleModes.push("gpai-user");
|
|
357
|
+
const providers = Array.from(
|
|
358
|
+
new Set(
|
|
359
|
+
signals
|
|
360
|
+
.filter((signal) => signal.category === "ai-provider")
|
|
361
|
+
.map((signal) => canonicalizeSignalName(signal))
|
|
362
|
+
)
|
|
363
|
+
);
|
|
364
|
+
gpaiIndicators.push({
|
|
365
|
+
hint: `GPAI model usage detected — cloud providers found: ${providers.join(", ")}. Provider obligations depend on your role and downstream distribution.`,
|
|
366
|
+
articleRef: "Art. 53",
|
|
367
|
+
confidence: "medium",
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (hasLocalInference) {
|
|
372
|
+
gpaiIndicators.push({
|
|
373
|
+
hint: "Local deployment detected — provider-side GPAI obligations are less likely unless you distribute or provide downstream access to the model.",
|
|
374
|
+
articleRef: "Art. 53",
|
|
375
|
+
confidence: "low",
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (complianceAreas.biasTesting.group.status !== "absent") {
|
|
380
|
+
riskIndicators.push({
|
|
381
|
+
hint: "Bias evaluation infrastructure detected — this can support Art. 10 evidence if it is part of your real evaluation workflow.",
|
|
382
|
+
articleRef: "Art. 10",
|
|
383
|
+
confidence: complianceAreas.biasTesting.group.confidence,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (signals.some((signal) => signal.category === "training")) {
|
|
388
|
+
riskIndicators.push({
|
|
389
|
+
hint: "Training or fine-tuning signals detected — data governance and provider obligations may apply depending on your role.",
|
|
390
|
+
articleRef: "Art. 10",
|
|
391
|
+
confidence: "high",
|
|
392
|
+
});
|
|
393
|
+
possibleModes.push("gpai-provider");
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
for (const [key, evaluation] of Object.entries(complianceAreas) as [
|
|
397
|
+
keyof typeof COMPLIANCE_AREA_MAP,
|
|
398
|
+
ComplianceAreaEvaluation,
|
|
399
|
+
][]) {
|
|
400
|
+
if (evaluation.implementationSignals.length > 0 || evaluation.supportingSignals.length === 0) {
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
riskIndicators.push({
|
|
405
|
+
hint: `Documentation references ${COMPLIANCE_AREA_LABELS[key]}, but no production implementation evidence was detected.`,
|
|
406
|
+
confidence: "low",
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (!possibleModes.includes("annex-iii-unclear")) {
|
|
411
|
+
possibleModes.push("annex-iii-unclear");
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return {
|
|
415
|
+
possibleModes,
|
|
416
|
+
riskIndicators,
|
|
417
|
+
gpaiIndicators,
|
|
418
|
+
openQuestions: openQuestions.map((question) => question.question),
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
interface GapRule {
|
|
423
|
+
id: string;
|
|
424
|
+
title: string;
|
|
425
|
+
description: string;
|
|
426
|
+
priority: ScanGap["priority"];
|
|
427
|
+
area: keyof typeof COMPLIANCE_AREA_MAP;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const GAP_RULES: GapRule[] = [
|
|
431
|
+
{
|
|
432
|
+
id: "gap-disclosure",
|
|
433
|
+
title: "No AI disclosure mechanism detected",
|
|
434
|
+
description:
|
|
435
|
+
"The EU AI Act requires that users are informed when interacting with AI systems. No user-facing disclosure implementation was found in the production codebase.",
|
|
436
|
+
priority: "critical",
|
|
437
|
+
area: "disclosure",
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
id: "gap-logging",
|
|
441
|
+
title: "No logging or monitoring infrastructure detected",
|
|
442
|
+
description:
|
|
443
|
+
"AI system outputs and decisions should be logged for traceability and audit purposes. No production logging infrastructure was detected.",
|
|
444
|
+
priority: "high",
|
|
445
|
+
area: "loggingMonitoring",
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
id: "gap-oversight",
|
|
449
|
+
title: "No human oversight mechanism detected",
|
|
450
|
+
description:
|
|
451
|
+
"High-risk AI systems require human oversight capabilities. No production review or override mechanisms were found.",
|
|
452
|
+
priority: "high",
|
|
453
|
+
area: "humanOversight",
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
id: "gap-bias",
|
|
457
|
+
title: "No bias testing infrastructure detected",
|
|
458
|
+
description:
|
|
459
|
+
"AI systems should be evaluated for bias and discrimination. No production-grade bias evaluation tooling was found.",
|
|
460
|
+
priority: "medium",
|
|
461
|
+
area: "biasTesting",
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
id: "gap-data",
|
|
465
|
+
title: "No data governance mechanisms detected",
|
|
466
|
+
description:
|
|
467
|
+
"Training and inference data should be governed with clear provenance and quality controls. No production data governance infrastructure was found.",
|
|
468
|
+
priority: "medium",
|
|
469
|
+
area: "dataGovernance",
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
id: "gap-incident",
|
|
473
|
+
title: "No incident reporting mechanism detected",
|
|
474
|
+
description:
|
|
475
|
+
"AI system incidents should be reportable and trackable. No production incident reporting infrastructure was found.",
|
|
476
|
+
priority: "medium",
|
|
477
|
+
area: "incidentReporting",
|
|
478
|
+
},
|
|
479
|
+
];
|
|
480
|
+
|
|
481
|
+
function buildGaps(
|
|
482
|
+
complianceAreas: Record<keyof typeof COMPLIANCE_AREA_MAP, ComplianceAreaEvaluation>,
|
|
483
|
+
aiDetected: boolean
|
|
484
|
+
): ScanGap[] {
|
|
485
|
+
if (!aiDetected) return [];
|
|
486
|
+
|
|
487
|
+
const gaps: ScanGap[] = [];
|
|
488
|
+
|
|
489
|
+
for (const rule of GAP_RULES) {
|
|
490
|
+
const evaluation = complianceAreas[rule.area];
|
|
491
|
+
const confidence = highestConfidence(evaluation.implementationSignals);
|
|
492
|
+
|
|
493
|
+
if (evaluation.implementationSignals.length === 0) {
|
|
494
|
+
gaps.push({
|
|
495
|
+
id: rule.id,
|
|
496
|
+
title: rule.title,
|
|
497
|
+
description: rule.description,
|
|
498
|
+
priority: rule.priority,
|
|
499
|
+
status: "missing",
|
|
500
|
+
basis: "scanner-rule",
|
|
501
|
+
evidence: [],
|
|
502
|
+
});
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (confidence !== "high") {
|
|
507
|
+
gaps.push({
|
|
508
|
+
id: rule.id,
|
|
509
|
+
title: `${rule.title.replace("No ", "Partial ")}`.replace(" detected", ""),
|
|
510
|
+
description: `Some implementation signals were found, but confidence is not yet high. ${rule.description}`,
|
|
511
|
+
priority:
|
|
512
|
+
rule.priority === "critical" ? "high" : rule.priority === "high" ? "medium" : "low",
|
|
513
|
+
status: "partial",
|
|
514
|
+
basis: "scanner-rule",
|
|
515
|
+
evidence: collectEvidence(evaluation.implementationSignals),
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return gaps;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function buildRecommendationSummary(
|
|
524
|
+
gaps: ScanGap[],
|
|
525
|
+
openQuestions: ScanResult["openQuestions"],
|
|
526
|
+
complianceAreas: Record<keyof typeof COMPLIANCE_AREA_MAP, ComplianceAreaEvaluation>
|
|
527
|
+
): string[] {
|
|
528
|
+
const recommendations: string[] = [];
|
|
529
|
+
const priorityOrder: Record<ScanGap["priority"], number> = {
|
|
530
|
+
critical: 0,
|
|
531
|
+
high: 1,
|
|
532
|
+
medium: 2,
|
|
533
|
+
low: 3,
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
const sortedGaps = [...gaps].sort(
|
|
537
|
+
(left, right) => priorityOrder[left.priority] - priorityOrder[right.priority]
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
for (const gap of sortedGaps.slice(0, 4)) {
|
|
541
|
+
const prefix = gap.status === "missing" ? "Implement" : "Strengthen";
|
|
542
|
+
recommendations.push(`[${gap.priority.toUpperCase()}] ${prefix}: ${gap.title}`);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (recommendations.length < 4) {
|
|
546
|
+
for (const [key, evaluation] of Object.entries(complianceAreas) as [
|
|
547
|
+
keyof typeof COMPLIANCE_AREA_MAP,
|
|
548
|
+
ComplianceAreaEvaluation,
|
|
549
|
+
][]) {
|
|
550
|
+
if (
|
|
551
|
+
evaluation.implementationSignals.length > 0 ||
|
|
552
|
+
evaluation.supportingSignals.length === 0
|
|
553
|
+
) {
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
recommendations.push(
|
|
558
|
+
`[LOW] Convert documented ${COMPLIANCE_AREA_LABELS[key]} into production implementation evidence`
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
if (recommendations.length >= 4) break;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (openQuestions.length > 0) {
|
|
566
|
+
recommendations.push(
|
|
567
|
+
`Resolve ${openQuestions.length} open question(s) that require human context`
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const finalRecommendation =
|
|
572
|
+
"Review the generated artifacts and complete role/risk classification in the EuConform web app";
|
|
573
|
+
|
|
574
|
+
return [...recommendations.slice(0, 4), finalRecommendation];
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function buildDetectedStack(signals: DetectedSignal[]): string[] {
|
|
578
|
+
const stack: string[] = [];
|
|
579
|
+
const seen = new Set<string>();
|
|
580
|
+
|
|
581
|
+
for (const signal of signals) {
|
|
582
|
+
if (
|
|
583
|
+
signal.category !== "framework" &&
|
|
584
|
+
signal.category !== "runtime" &&
|
|
585
|
+
signal.category !== "ai-framework"
|
|
586
|
+
) {
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const canonicalName = canonicalizeSignalName(signal);
|
|
591
|
+
const key = canonicalName.toLowerCase();
|
|
592
|
+
if (seen.has(key)) continue;
|
|
593
|
+
seen.add(key);
|
|
594
|
+
stack.push(canonicalName);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return stack;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function hasCapability(group: ComplianceSignalGroup): boolean {
|
|
601
|
+
return group.status === "present" || group.status === "partial";
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function resolveBomKind(signal: DetectedSignal, canonicalName: string): BomComponentKind {
|
|
605
|
+
if (signal.category === "rag") {
|
|
606
|
+
return canonicalName.toLowerCase().includes("embedding") ? "embedding" : "vector-store";
|
|
607
|
+
}
|
|
608
|
+
return CATEGORY_TO_BOM_KIND[signal.category] ?? "tool";
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function buildAiBom(
|
|
612
|
+
scanResult: ScanResult,
|
|
613
|
+
complianceAreas: Record<keyof typeof COMPLIANCE_AREA_MAP, ComplianceAreaEvaluation>,
|
|
614
|
+
generatedAt: string
|
|
615
|
+
): AiBillOfMaterials {
|
|
616
|
+
const components: BomComponent[] = [];
|
|
617
|
+
const componentIndex = new Map<string, number>();
|
|
618
|
+
|
|
619
|
+
for (const signal of scanResult.signals) {
|
|
620
|
+
if (!BOM_CATEGORIES.has(signal.category)) continue;
|
|
621
|
+
if (signal.category === "rag" && signal.confidence === "low") continue;
|
|
622
|
+
|
|
623
|
+
const canonicalName = canonicalizeSignalName(signal);
|
|
624
|
+
const kind = resolveBomKind(signal, canonicalName);
|
|
625
|
+
const key = `${kind}:${canonicalName.toLowerCase()}`;
|
|
626
|
+
const source = deriveSource(signal.evidence);
|
|
627
|
+
const existingIndex = componentIndex.get(key);
|
|
628
|
+
|
|
629
|
+
if (existingIndex === undefined) {
|
|
630
|
+
componentIndex.set(key, components.length);
|
|
631
|
+
components.push({
|
|
632
|
+
id: makeBomComponentId(kind, canonicalName),
|
|
633
|
+
kind,
|
|
634
|
+
name: canonicalName,
|
|
635
|
+
source,
|
|
636
|
+
});
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const existing = components[existingIndex];
|
|
641
|
+
if (!existing) continue;
|
|
642
|
+
if (SOURCE_PRIORITY[source] > SOURCE_PRIORITY[existing.source]) {
|
|
643
|
+
existing.source = source;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const reportingSignals = complianceAreas.reportingExports.implementationSignals;
|
|
648
|
+
|
|
649
|
+
return {
|
|
650
|
+
schemaVersion: "euconform.aibom.v1",
|
|
651
|
+
generatedAt,
|
|
652
|
+
project: {
|
|
653
|
+
name: scanResult.repo.name,
|
|
654
|
+
rootPath: scanResult.meta.targetPath,
|
|
655
|
+
},
|
|
656
|
+
components: [...components].sort((left, right) =>
|
|
657
|
+
`${left.kind}:${left.name}`.localeCompare(`${right.kind}:${right.name}`)
|
|
658
|
+
),
|
|
659
|
+
complianceCapabilities: {
|
|
660
|
+
biasEvaluation: hasCapability(complianceAreas.biasTesting.group),
|
|
661
|
+
jsonExport: reportingSignals.some((signal) => signal.id.includes("json")),
|
|
662
|
+
pdfExport: reportingSignals.some((signal) => signal.id.includes("pdf")),
|
|
663
|
+
loggingInfrastructure: hasCapability(complianceAreas.loggingMonitoring.group),
|
|
664
|
+
humanReviewFlow: hasCapability(complianceAreas.humanOversight.group),
|
|
665
|
+
incidentHandling: hasCapability(complianceAreas.incidentReporting.group),
|
|
666
|
+
},
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
export function generateScanOutput(scanResult: ScanResult): ScanOutput {
|
|
671
|
+
const generatedAt = new Date().toISOString();
|
|
672
|
+
const aiDetected = hasAI(scanResult.signals);
|
|
673
|
+
const complianceAreas = evaluateComplianceAreas(scanResult.signals, aiDetected);
|
|
674
|
+
|
|
675
|
+
const report: ScanReport = {
|
|
676
|
+
schemaVersion: "euconform.report.v1",
|
|
677
|
+
generatedAt,
|
|
678
|
+
tool: {
|
|
679
|
+
name: "euconform",
|
|
680
|
+
version: scanResult.meta.toolVersion,
|
|
681
|
+
},
|
|
682
|
+
target: {
|
|
683
|
+
rootPath: scanResult.meta.targetPath,
|
|
684
|
+
name: scanResult.repo.name,
|
|
685
|
+
repoType: scanResult.appTypes[0] ?? "unknown",
|
|
686
|
+
detectedStack: buildDetectedStack(scanResult.signals),
|
|
687
|
+
},
|
|
688
|
+
aiFootprint: buildAiFootprint(scanResult.signals),
|
|
689
|
+
complianceSignals: {
|
|
690
|
+
disclosure: complianceAreas.disclosure.group,
|
|
691
|
+
biasTesting: complianceAreas.biasTesting.group,
|
|
692
|
+
reportingExports: complianceAreas.reportingExports.group,
|
|
693
|
+
loggingMonitoring: complianceAreas.loggingMonitoring.group,
|
|
694
|
+
humanOversight: complianceAreas.humanOversight.group,
|
|
695
|
+
dataGovernance: complianceAreas.dataGovernance.group,
|
|
696
|
+
incidentReporting: complianceAreas.incidentReporting.group,
|
|
697
|
+
},
|
|
698
|
+
assessmentHints: buildAssessmentHints(
|
|
699
|
+
scanResult.signals,
|
|
700
|
+
complianceAreas,
|
|
701
|
+
scanResult.openQuestions
|
|
702
|
+
),
|
|
703
|
+
gaps: buildGaps(complianceAreas, aiDetected),
|
|
704
|
+
recommendationSummary: [],
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
report.recommendationSummary = buildRecommendationSummary(
|
|
708
|
+
report.gaps,
|
|
709
|
+
scanResult.openQuestions,
|
|
710
|
+
complianceAreas
|
|
711
|
+
);
|
|
712
|
+
|
|
713
|
+
const aibom = buildAiBom(scanResult, complianceAreas, generatedAt);
|
|
714
|
+
const summaryMarkdown = generateSummaryMarkdown(report, aibom, scanResult.meta.scanScope);
|
|
715
|
+
|
|
716
|
+
return { report, aibom, summaryMarkdown };
|
|
717
|
+
}
|