@euconform/core 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +18 -0
  3. package/assets/datasets/crows-pairs-de.json +613 -0
  4. package/assets/datasets/crows-pairs-en.json +9055 -0
  5. package/dist/datasets/crows-pairs.d.ts +7 -0
  6. package/dist/datasets/crows-pairs.d.ts.map +1 -0
  7. package/dist/datasets/crows-pairs.js +41 -0
  8. package/dist/evidence/bundle.d.ts +23 -0
  9. package/dist/evidence/bundle.d.ts.map +1 -0
  10. package/dist/evidence/bundle.js +44 -0
  11. package/dist/evidence/compliance-evaluation.d.ts +12 -0
  12. package/dist/evidence/compliance-evaluation.d.ts.map +1 -0
  13. package/dist/evidence/compliance-evaluation.js +78 -0
  14. package/dist/evidence/hash.d.ts +2 -0
  15. package/dist/evidence/hash.d.ts.map +1 -0
  16. package/dist/evidence/hash.js +4 -0
  17. package/dist/evidence/index.d.ts +13 -0
  18. package/dist/evidence/index.d.ts.map +1 -0
  19. package/dist/evidence/index.js +11 -0
  20. package/dist/evidence/markdown.d.ts +9 -0
  21. package/dist/evidence/markdown.d.ts.map +1 -0
  22. package/dist/evidence/markdown.js +169 -0
  23. package/dist/evidence/output.d.ts +13 -0
  24. package/dist/evidence/output.d.ts.map +1 -0
  25. package/dist/evidence/output.js +547 -0
  26. package/dist/evidence/types.d.ts +230 -0
  27. package/dist/evidence/types.d.ts.map +1 -0
  28. package/dist/evidence/types.js +7 -0
  29. package/dist/evidence/validate.d.ts +8 -0
  30. package/dist/evidence/validate.d.ts.map +1 -0
  31. package/dist/evidence/validate.js +192 -0
  32. package/dist/index.d.ts +1 -1
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/inference/index.d.ts +1 -1
  35. package/dist/inference/index.d.ts.map +1 -1
  36. package/dist/inference/index.js +1 -1
  37. package/dist/inference/ollama-client.d.ts +11 -3
  38. package/dist/inference/ollama-client.d.ts.map +1 -1
  39. package/dist/inference/ollama-client.js +84 -84
  40. package/dist/scanner/aggregator.d.ts +7 -0
  41. package/dist/scanner/aggregator.d.ts.map +1 -0
  42. package/dist/scanner/aggregator.js +200 -0
  43. package/dist/scanner/detectors/ai-framework-detector.d.ts +3 -0
  44. package/dist/scanner/detectors/ai-framework-detector.d.ts.map +1 -0
  45. package/dist/scanner/detectors/ai-framework-detector.js +74 -0
  46. package/dist/scanner/detectors/ai-provider-detector.d.ts +3 -0
  47. package/dist/scanner/detectors/ai-provider-detector.d.ts.map +1 -0
  48. package/dist/scanner/detectors/ai-provider-detector.js +100 -0
  49. package/dist/scanner/detectors/compliance-detector.d.ts +3 -0
  50. package/dist/scanner/detectors/compliance-detector.d.ts.map +1 -0
  51. package/dist/scanner/detectors/compliance-detector.js +201 -0
  52. package/dist/scanner/detectors/config-detector.d.ts +3 -0
  53. package/dist/scanner/detectors/config-detector.d.ts.map +1 -0
  54. package/dist/scanner/detectors/config-detector.js +248 -0
  55. package/dist/scanner/detectors/framework-detector.d.ts +3 -0
  56. package/dist/scanner/detectors/framework-detector.d.ts.map +1 -0
  57. package/dist/scanner/detectors/framework-detector.js +141 -0
  58. package/dist/scanner/detectors/index.d.ts +8 -0
  59. package/dist/scanner/detectors/index.d.ts.map +1 -0
  60. package/dist/scanner/detectors/index.js +37 -0
  61. package/dist/scanner/detectors/local-inference-detector.d.ts +3 -0
  62. package/dist/scanner/detectors/local-inference-detector.d.ts.map +1 -0
  63. package/dist/scanner/detectors/local-inference-detector.js +93 -0
  64. package/dist/scanner/detectors/markdown-detector.d.ts +3 -0
  65. package/dist/scanner/detectors/markdown-detector.d.ts.map +1 -0
  66. package/dist/scanner/detectors/markdown-detector.js +139 -0
  67. package/dist/scanner/detectors/package-json-detector.d.ts +3 -0
  68. package/dist/scanner/detectors/package-json-detector.d.ts.map +1 -0
  69. package/dist/scanner/detectors/package-json-detector.js +166 -0
  70. package/dist/scanner/detectors/rag-detector.d.ts +3 -0
  71. package/dist/scanner/detectors/rag-detector.d.ts.map +1 -0
  72. package/dist/scanner/detectors/rag-detector.js +132 -0
  73. package/dist/scanner/detectors/shared.d.ts +18 -0
  74. package/dist/scanner/detectors/shared.d.ts.map +1 -0
  75. package/dist/scanner/detectors/shared.js +36 -0
  76. package/dist/scanner/discovery.d.ts +14 -0
  77. package/dist/scanner/discovery.d.ts.map +1 -0
  78. package/dist/scanner/discovery.js +235 -0
  79. package/dist/scanner/index.d.ts +9 -0
  80. package/dist/scanner/index.d.ts.map +1 -0
  81. package/dist/scanner/index.js +8 -0
  82. package/package.json +8 -3
  83. package/src/datasets/crows-pairs.ts +64 -0
  84. package/src/evidence/bundle.ts +65 -0
  85. package/src/evidence/compliance-evaluation.ts +114 -0
  86. package/src/evidence/hash.ts +5 -0
  87. package/src/evidence/index.ts +51 -0
  88. package/src/evidence/markdown.ts +226 -0
  89. package/src/evidence/output.ts +717 -0
  90. package/src/evidence/types.ts +329 -0
  91. package/src/evidence/validate.ts +300 -0
  92. package/src/index.ts +1 -0
  93. package/src/inference/index.ts +6 -1
  94. package/src/inference/ollama-client.ts +111 -104
  95. package/src/scanner/aggregator.ts +258 -0
  96. package/src/scanner/detectors/ai-framework-detector.ts +94 -0
  97. package/src/scanner/detectors/ai-provider-detector.ts +135 -0
  98. package/src/scanner/detectors/compliance-detector.ts +232 -0
  99. package/src/scanner/detectors/config-detector.ts +316 -0
  100. package/src/scanner/detectors/framework-detector.ts +163 -0
  101. package/src/scanner/detectors/index.ts +40 -0
  102. package/src/scanner/detectors/local-inference-detector.ts +124 -0
  103. package/src/scanner/detectors/markdown-detector.ts +176 -0
  104. package/src/scanner/detectors/package-json-detector.ts +202 -0
  105. package/src/scanner/detectors/rag-detector.ts +162 -0
  106. package/src/scanner/detectors/shared.ts +45 -0
  107. package/src/scanner/discovery.ts +290 -0
  108. package/src/scanner/index.ts +9 -0
  109. package/tests/evidence/bundle.test.ts +128 -0
  110. package/tests/evidence/hash.test.ts +25 -0
  111. package/tests/evidence/markdown.test.ts +92 -0
  112. package/tests/evidence/output.test.ts +203 -0
  113. package/tests/evidence/spec-schemas.test.ts +316 -0
  114. package/tests/fixtures/compliance-good/README.md +14 -0
  115. package/tests/fixtures/compliance-good/lib/ai-service.ts +17 -0
  116. package/tests/fixtures/compliance-good/lib/review-flow.ts +11 -0
  117. package/tests/fixtures/compliance-good/package.json +10 -0
  118. package/tests/fixtures/compliance-good/tests/bias-check.test.ts +13 -0
  119. package/tests/fixtures/nextjs-openai/README.md +5 -0
  120. package/tests/fixtures/nextjs-openai/app/page.tsx +11 -0
  121. package/tests/fixtures/nextjs-openai/package.json +9 -0
  122. package/tests/fixtures/ollama-rag/.env.example +2 -0
  123. package/tests/fixtures/ollama-rag/docker-compose.yml +9 -0
  124. package/tests/fixtures/ollama-rag/package.json +9 -0
  125. package/tests/fixtures/ollama-rag/server.ts +21 -0
  126. package/tests/fixtures/plain-webapp/README.md +3 -0
  127. package/tests/fixtures/plain-webapp/index.js +5 -0
  128. package/tests/fixtures/plain-webapp/package.json +8 -0
  129. package/tests/gpai-classifier.test.ts +4 -4
  130. package/tests/gpai-timeline.test.ts +2 -2
  131. package/tests/scanner/aggregator.test.ts +122 -0
  132. package/tests/scanner/detectors.test.ts +274 -0
  133. package/tests/scanner/discovery.test.ts +87 -0
  134. package/vitest.config.ts +1 -0
@@ -0,0 +1,329 @@
1
+ /**
2
+ * EuConform Evidence Engine — Type Definitions
3
+ *
4
+ * The scanner produces evidence, hints, and open questions — not legal verdicts.
5
+ * Classification is done by the wizard from human input, not by the scanner.
6
+ */
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Signal types
10
+ // ---------------------------------------------------------------------------
11
+
12
+ export type SignalCategory =
13
+ | "framework"
14
+ | "runtime"
15
+ | "ai-provider"
16
+ | "ai-framework"
17
+ | "ai-model"
18
+ | "local-inference"
19
+ | "rag"
20
+ | "training"
21
+ | "compliance-disclosure"
22
+ | "compliance-logging"
23
+ | "compliance-oversight"
24
+ | "compliance-bias"
25
+ | "compliance-data"
26
+ | "compliance-incident"
27
+ | "compliance-reporting";
28
+
29
+ export type ConfidenceLevel = "high" | "medium" | "low";
30
+
31
+ export type ScanScope = "production" | "all";
32
+
33
+ export type FileProvenance =
34
+ | "runtime"
35
+ | "config"
36
+ | "root-docs"
37
+ | "docs"
38
+ | "test"
39
+ | "fixture"
40
+ | "example"
41
+ | "generated"
42
+ | "tooling";
43
+
44
+ export interface SignalEvidence {
45
+ file: string;
46
+ line?: number;
47
+ snippet: string;
48
+ provenance?: FileProvenance;
49
+ }
50
+
51
+ export interface DetectedSignal {
52
+ id: string;
53
+ name: string;
54
+ category: SignalCategory;
55
+ confidence: ConfidenceLevel;
56
+ evidence: SignalEvidence[];
57
+ }
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // Open questions
61
+ // ---------------------------------------------------------------------------
62
+
63
+ export interface OpenQuestion {
64
+ id: string;
65
+ question: string;
66
+ context: string;
67
+ relatedSignalIds: string[];
68
+ suggestedAction: string;
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Scanner result (raw output of scanRepository)
73
+ // ---------------------------------------------------------------------------
74
+
75
+ export interface ScanMeta {
76
+ scannedAt: string;
77
+ scanDurationMs: number;
78
+ toolVersion: string;
79
+ targetPath: string;
80
+ scanScope: ScanScope;
81
+ filesScanned: number;
82
+ filesSkipped: number;
83
+ }
84
+
85
+ export interface RepoProfile {
86
+ name: string;
87
+ version?: string;
88
+ description?: string;
89
+ license?: string;
90
+ isMonorepo: boolean;
91
+ packageManager?: "npm" | "pnpm" | "yarn" | "bun";
92
+ languages: string[];
93
+ }
94
+
95
+ export type AppType =
96
+ | "web-app"
97
+ | "api-server"
98
+ | "cli-tool"
99
+ | "library"
100
+ | "chatbot"
101
+ | "ml-pipeline"
102
+ | "unknown";
103
+
104
+ export interface ScanResult {
105
+ meta: ScanMeta;
106
+ repo: RepoProfile;
107
+ appTypes: AppType[];
108
+ signals: DetectedSignal[];
109
+ openQuestions: OpenQuestion[];
110
+ signalSummary: {
111
+ total: number;
112
+ byCategory: Partial<Record<SignalCategory, number>>;
113
+ byConfidence: Record<ConfidenceLevel, number>;
114
+ };
115
+ }
116
+
117
+ // ---------------------------------------------------------------------------
118
+ // File discovery
119
+ // ---------------------------------------------------------------------------
120
+
121
+ export interface ScanOptions {
122
+ targetPath: string;
123
+ scope?: ScanScope;
124
+ extensions?: string[];
125
+ excludeGlobs?: string[];
126
+ maxFileSizeBytes?: number;
127
+ maxFiles?: number;
128
+ }
129
+
130
+ export interface ScanFile {
131
+ relativePath: string;
132
+ absolutePath: string;
133
+ extension: string;
134
+ content: string;
135
+ sizeBytes: number;
136
+ provenance: FileProvenance;
137
+ }
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // Detector interface
141
+ // ---------------------------------------------------------------------------
142
+
143
+ export interface DetectorContext {
144
+ file: ScanFile;
145
+ /** Pre-split lines of file.content — avoids redundant .split("\n") in each detector */
146
+ lines: string[];
147
+ allFiles: ScanFile[];
148
+ /** Pre-parsed root package.json, if available */
149
+ rootPackageJson?: Record<string, unknown>;
150
+ }
151
+
152
+ export type SignalDetector = (ctx: DetectorContext) => DetectedSignal[];
153
+
154
+ // ---------------------------------------------------------------------------
155
+ // Scan report (euconform.report.v1)
156
+ // ---------------------------------------------------------------------------
157
+
158
+ export interface ComplianceSignalGroup {
159
+ status: "present" | "partial" | "absent" | "unknown";
160
+ confidence: ConfidenceLevel;
161
+ evidence: SignalEvidence[];
162
+ }
163
+
164
+ export interface AssessmentHint {
165
+ hint: string;
166
+ articleRef?: string;
167
+ confidence: ConfidenceLevel;
168
+ }
169
+
170
+ export interface ScanGap {
171
+ id: string;
172
+ title: string;
173
+ description: string;
174
+ priority: "critical" | "high" | "medium" | "low";
175
+ status: "missing" | "partial";
176
+ basis: "scanner-rule";
177
+ evidence: SignalEvidence[];
178
+ }
179
+
180
+ export interface ScanReport {
181
+ schemaVersion: "euconform.report.v1";
182
+ generatedAt: string;
183
+ tool: { name: string; version: string };
184
+ target: {
185
+ rootPath: string;
186
+ name: string;
187
+ repoType: string;
188
+ detectedStack: string[];
189
+ };
190
+ aiFootprint: {
191
+ usesAI: boolean;
192
+ inferenceModes: string[];
193
+ providerHints: string[];
194
+ ragHints: string[];
195
+ };
196
+ complianceSignals: {
197
+ disclosure: ComplianceSignalGroup;
198
+ biasTesting: ComplianceSignalGroup;
199
+ reportingExports: ComplianceSignalGroup;
200
+ loggingMonitoring: ComplianceSignalGroup;
201
+ humanOversight: ComplianceSignalGroup;
202
+ dataGovernance: ComplianceSignalGroup;
203
+ incidentReporting: ComplianceSignalGroup;
204
+ };
205
+ assessmentHints: {
206
+ possibleModes: string[];
207
+ riskIndicators: AssessmentHint[];
208
+ gpaiIndicators: AssessmentHint[];
209
+ openQuestions: string[];
210
+ };
211
+ gaps: ScanGap[];
212
+ recommendationSummary: string[];
213
+ }
214
+
215
+ // ---------------------------------------------------------------------------
216
+ // AI Bill of Materials (euconform.aibom.v1)
217
+ // ---------------------------------------------------------------------------
218
+
219
+ export type BomComponentKind =
220
+ | "framework"
221
+ | "runtime"
222
+ | "inference-provider"
223
+ | "ai-framework"
224
+ | "model"
225
+ | "vector-store"
226
+ | "embedding"
227
+ | "dataset"
228
+ | "tool";
229
+
230
+ export interface BomComponent {
231
+ id: string;
232
+ kind: BomComponentKind;
233
+ name: string;
234
+ version?: string;
235
+ source: "package.json" | "lock-file" | "code" | "config" | "docs";
236
+ }
237
+
238
+ export interface AiBillOfMaterials {
239
+ schemaVersion: "euconform.aibom.v1";
240
+ generatedAt: string;
241
+ project: {
242
+ name: string;
243
+ rootPath: string;
244
+ };
245
+ components: BomComponent[];
246
+ complianceCapabilities: {
247
+ biasEvaluation: boolean;
248
+ jsonExport: boolean;
249
+ pdfExport: boolean;
250
+ loggingInfrastructure: boolean;
251
+ humanReviewFlow: boolean;
252
+ incidentHandling: boolean;
253
+ };
254
+ }
255
+
256
+ // ---------------------------------------------------------------------------
257
+ // CI report (euconform.ci.v1)
258
+ // ---------------------------------------------------------------------------
259
+
260
+ export type FailOnLevel = "none" | "critical" | "high" | "medium" | "low";
261
+
262
+ export interface GapCounts {
263
+ critical: number;
264
+ high: number;
265
+ medium: number;
266
+ low: number;
267
+ }
268
+
269
+ export interface CiReport {
270
+ schemaVersion: "euconform.ci.v1";
271
+ generatedAt: string;
272
+ target: {
273
+ name: string;
274
+ rootPath: string;
275
+ };
276
+ status: {
277
+ failOn: FailOnLevel;
278
+ failing: boolean;
279
+ gapCounts: GapCounts;
280
+ openQuestions: number;
281
+ };
282
+ aiDetected: boolean;
283
+ scanScope: ScanScope;
284
+ artifacts: string[];
285
+ complianceOverview: Array<{
286
+ area: string;
287
+ status: ComplianceSignalGroup["status"];
288
+ confidence: ComplianceSignalGroup["confidence"];
289
+ }>;
290
+ topGaps: Array<{
291
+ id: string;
292
+ title: string;
293
+ priority: ScanGap["priority"];
294
+ status: ScanGap["status"];
295
+ }>;
296
+ }
297
+
298
+ // ---------------------------------------------------------------------------
299
+ // Combined scan output
300
+ // ---------------------------------------------------------------------------
301
+
302
+ export interface ScanOutput {
303
+ report: ScanReport;
304
+ aibom: AiBillOfMaterials;
305
+ summaryMarkdown: string;
306
+ }
307
+
308
+ // ---------------------------------------------------------------------------
309
+ // Bundle manifest (euconform.bundle.v1)
310
+ // ---------------------------------------------------------------------------
311
+
312
+ export type BundleArtifactRole = "report" | "aibom" | "ci" | "summary";
313
+
314
+ export interface BundleArtifactRef {
315
+ role: BundleArtifactRole;
316
+ fileName: string;
317
+ sha256: string;
318
+ schemaVersion?: string;
319
+ mimeType?: string;
320
+ required: boolean;
321
+ }
322
+
323
+ export interface ScanBundle {
324
+ schemaVersion: "euconform.bundle.v1";
325
+ generatedAt: string;
326
+ tool: { name: string; version: string };
327
+ target: { name: string; rootPath: string };
328
+ artifacts: BundleArtifactRef[];
329
+ }
@@ -0,0 +1,300 @@
1
+ import type { AiBillOfMaterials, CiReport, ScanBundle, ScanReport } from "./types";
2
+
3
+ function assertObject(data: unknown, label: string): Record<string, unknown> {
4
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
5
+ throw new Error(`Invalid ${label}: expected a JSON object`);
6
+ }
7
+ return data as Record<string, unknown>;
8
+ }
9
+
10
+ function requireSchemaVersion(obj: Record<string, unknown>, expected: string, label: string): void {
11
+ if (obj.schemaVersion !== expected) {
12
+ throw new Error(
13
+ `Invalid ${label} schema version: expected "${expected}", got "${String(obj.schemaVersion)}"`
14
+ );
15
+ }
16
+ }
17
+
18
+ function requireField(
19
+ obj: Record<string, unknown>,
20
+ key: string,
21
+ type: "string" | "boolean" | "object" | "array" | "number",
22
+ label: string,
23
+ fieldName = key
24
+ ): unknown {
25
+ if (!(key in obj)) {
26
+ throw new Error(`Invalid ${label}: missing '${fieldName}'`);
27
+ }
28
+
29
+ const value = obj[key];
30
+ const isValid =
31
+ (type === "array" && Array.isArray(value)) ||
32
+ (type === "object" && value && typeof value === "object" && !Array.isArray(value)) ||
33
+ (type === "string" && typeof value === "string") ||
34
+ (type === "boolean" && typeof value === "boolean") ||
35
+ (type === "number" && typeof value === "number");
36
+
37
+ if (!isValid) {
38
+ throw new Error(`Invalid ${label}: '${fieldName}' must be a ${type}`);
39
+ }
40
+
41
+ return value;
42
+ }
43
+
44
+ function requireStringArray(
45
+ obj: Record<string, unknown>,
46
+ key: string,
47
+ label: string,
48
+ fieldName = key
49
+ ): string[] {
50
+ const value = requireField(obj, key, "array", label, fieldName) as unknown[];
51
+ if (!value.every((item) => typeof item === "string")) {
52
+ throw new Error(`Invalid ${label}: '${fieldName}' must contain strings`);
53
+ }
54
+ return value as string[];
55
+ }
56
+
57
+ function majorVersion(schemaVersion: string): string | null {
58
+ const match = /\.v(\d+)$/.exec(schemaVersion);
59
+ return match?.[1] ?? null;
60
+ }
61
+
62
+ export function getSchemaMajorVersion(schemaVersion: string): string | null {
63
+ return majorVersion(schemaVersion);
64
+ }
65
+
66
+ export function validateScanReport(data: unknown): ScanReport {
67
+ const obj = assertObject(data, "report");
68
+ requireSchemaVersion(obj, "euconform.report.v1", "report");
69
+ requireField(obj, "generatedAt", "string", "report");
70
+
71
+ const tool = requireField(obj, "tool", "object", "report") as Record<string, unknown>;
72
+ requireField(tool, "name", "string", "report", "tool.name");
73
+ requireField(tool, "version", "string", "report", "tool.version");
74
+
75
+ const target = requireField(obj, "target", "object", "report") as Record<string, unknown>;
76
+ requireField(target, "rootPath", "string", "report", "target.rootPath");
77
+ requireField(target, "name", "string", "report", "target.name");
78
+ requireField(target, "repoType", "string", "report", "target.repoType");
79
+ requireField(target, "detectedStack", "array", "report", "target.detectedStack");
80
+
81
+ const aiFootprint = requireField(obj, "aiFootprint", "object", "report") as Record<
82
+ string,
83
+ unknown
84
+ >;
85
+ requireField(aiFootprint, "usesAI", "boolean", "report", "aiFootprint.usesAI");
86
+ requireStringArray(aiFootprint, "inferenceModes", "report", "aiFootprint.inferenceModes");
87
+ requireStringArray(aiFootprint, "providerHints", "report", "aiFootprint.providerHints");
88
+ requireStringArray(aiFootprint, "ragHints", "report", "aiFootprint.ragHints");
89
+
90
+ requireField(obj, "complianceSignals", "object", "report");
91
+ const hints = requireField(obj, "assessmentHints", "object", "report") as Record<string, unknown>;
92
+ requireStringArray(hints, "possibleModes", "report", "assessmentHints.possibleModes");
93
+ requireField(hints, "riskIndicators", "array", "report", "assessmentHints.riskIndicators");
94
+ requireField(hints, "gpaiIndicators", "array", "report", "assessmentHints.gpaiIndicators");
95
+ requireStringArray(hints, "openQuestions", "report", "assessmentHints.openQuestions");
96
+
97
+ requireField(obj, "gaps", "array", "report");
98
+ requireStringArray(obj, "recommendationSummary", "report");
99
+
100
+ return data as ScanReport;
101
+ }
102
+
103
+ export function validateAiBillOfMaterials(data: unknown): AiBillOfMaterials {
104
+ const obj = assertObject(data, "AIBOM");
105
+ requireSchemaVersion(obj, "euconform.aibom.v1", "AIBOM");
106
+ requireField(obj, "generatedAt", "string", "AIBOM");
107
+
108
+ const project = requireField(obj, "project", "object", "AIBOM") as Record<string, unknown>;
109
+ requireField(project, "name", "string", "AIBOM", "project.name");
110
+ requireField(project, "rootPath", "string", "AIBOM", "project.rootPath");
111
+
112
+ requireField(obj, "components", "array", "AIBOM");
113
+ requireField(obj, "complianceCapabilities", "object", "AIBOM");
114
+
115
+ return data as AiBillOfMaterials;
116
+ }
117
+
118
+ export function validateCiReport(data: unknown): CiReport {
119
+ const obj = assertObject(data, "CI report");
120
+ requireSchemaVersion(obj, "euconform.ci.v1", "CI report");
121
+ requireField(obj, "generatedAt", "string", "CI report");
122
+
123
+ const target = requireField(obj, "target", "object", "CI report") as Record<string, unknown>;
124
+ requireField(target, "name", "string", "CI report", "target.name");
125
+ requireField(target, "rootPath", "string", "CI report", "target.rootPath");
126
+
127
+ const status = requireField(obj, "status", "object", "CI report") as Record<string, unknown>;
128
+ requireField(status, "failOn", "string", "CI report", "status.failOn");
129
+ requireField(status, "failing", "boolean", "CI report", "status.failing");
130
+ requireField(status, "openQuestions", "number", "CI report", "status.openQuestions");
131
+
132
+ const gapCounts = requireField(
133
+ status,
134
+ "gapCounts",
135
+ "object",
136
+ "CI report",
137
+ "status.gapCounts"
138
+ ) as Record<string, unknown>;
139
+ for (const key of ["critical", "high", "medium", "low"] as const) {
140
+ requireField(gapCounts, key, "number", "CI report", `status.gapCounts.${key}`);
141
+ }
142
+
143
+ requireField(obj, "aiDetected", "boolean", "CI report");
144
+ requireField(obj, "scanScope", "string", "CI report");
145
+ requireField(obj, "artifacts", "array", "CI report");
146
+ requireField(obj, "complianceOverview", "array", "CI report");
147
+ requireField(obj, "topGaps", "array", "CI report");
148
+
149
+ return data as CiReport;
150
+ }
151
+
152
+ const VALID_BUNDLE_ROLES = new Set(["report", "aibom", "ci", "summary"]);
153
+ const SHA256_HEX = /^[a-f0-9]{64}$/;
154
+
155
+ interface BundleArtifactFields {
156
+ role: string;
157
+ fileName: string;
158
+ sha256: string;
159
+ required: boolean;
160
+ declaredSchemaVersion: string | undefined;
161
+ mimeType: string | undefined;
162
+ }
163
+
164
+ function readBundleArtifactFields(
165
+ raw: Record<string, unknown>,
166
+ index: number
167
+ ): BundleArtifactFields {
168
+ const scope = "bundle artifact";
169
+ const role = requireField(raw, "role", "string", scope, `artifacts[${index}].role`) as string;
170
+ const fileName = requireField(
171
+ raw,
172
+ "fileName",
173
+ "string",
174
+ scope,
175
+ `artifacts[${index}].fileName`
176
+ ) as string;
177
+ const sha256 = requireField(
178
+ raw,
179
+ "sha256",
180
+ "string",
181
+ scope,
182
+ `artifacts[${index}].sha256`
183
+ ) as string;
184
+ const required = requireField(
185
+ raw,
186
+ "required",
187
+ "boolean",
188
+ scope,
189
+ `artifacts[${index}].required`
190
+ ) as boolean;
191
+
192
+ const declaredSchemaVersion = raw.schemaVersion;
193
+ if (declaredSchemaVersion !== undefined && typeof declaredSchemaVersion !== "string") {
194
+ throw new Error(`Invalid bundle artifact: '${fileName}' has non-string schemaVersion`);
195
+ }
196
+ if (raw.mimeType !== undefined && typeof raw.mimeType !== "string") {
197
+ throw new Error(`Invalid bundle artifact: '${fileName}' has non-string mimeType`);
198
+ }
199
+
200
+ return {
201
+ role,
202
+ fileName,
203
+ sha256,
204
+ required,
205
+ declaredSchemaVersion: declaredSchemaVersion as string | undefined,
206
+ mimeType: raw.mimeType as string | undefined,
207
+ };
208
+ }
209
+
210
+ function assertBundleArtifactIntegrity(
211
+ fields: BundleArtifactFields,
212
+ seenRoles: Set<string>,
213
+ seenFiles: Set<string>,
214
+ bundleMajor: string | null
215
+ ): void {
216
+ const { role, fileName, sha256, declaredSchemaVersion } = fields;
217
+
218
+ if (!VALID_BUNDLE_ROLES.has(role)) {
219
+ throw new Error(`Invalid bundle artifact: unsupported role '${role}'`);
220
+ }
221
+ if (!SHA256_HEX.test(sha256)) {
222
+ throw new Error(`Invalid bundle artifact: '${fileName}' has invalid sha256`);
223
+ }
224
+ if (seenRoles.has(role)) {
225
+ throw new Error(`Invalid bundle artifact: duplicate role '${role}'`);
226
+ }
227
+ if (seenFiles.has(fileName)) {
228
+ throw new Error(`Invalid bundle artifact: duplicate fileName '${fileName}'`);
229
+ }
230
+ if (declaredSchemaVersion && bundleMajor && majorVersion(declaredSchemaVersion) !== bundleMajor) {
231
+ throw new Error(
232
+ `Invalid bundle artifact: '${fileName}' uses schemaVersion '${declaredSchemaVersion}' with a different major than the bundle`
233
+ );
234
+ }
235
+ }
236
+
237
+ function validateBundleHeader(obj: Record<string, unknown>): void {
238
+ requireSchemaVersion(obj, "euconform.bundle.v1", "bundle");
239
+ requireField(obj, "generatedAt", "string", "bundle");
240
+
241
+ const tool = requireField(obj, "tool", "object", "bundle") as Record<string, unknown>;
242
+ requireField(tool, "name", "string", "bundle", "tool.name");
243
+ requireField(tool, "version", "string", "bundle", "tool.version");
244
+
245
+ const target = requireField(obj, "target", "object", "bundle") as Record<string, unknown>;
246
+ requireField(target, "name", "string", "bundle", "target.name");
247
+ requireField(target, "rootPath", "string", "bundle", "target.rootPath");
248
+ }
249
+
250
+ export function validateScanBundle(data: unknown): ScanBundle {
251
+ const obj = assertObject(data, "bundle");
252
+ validateBundleHeader(obj);
253
+
254
+ const artifacts = requireField(obj, "artifacts", "array", "bundle") as unknown[];
255
+ if (artifacts.length === 0) {
256
+ throw new Error("Invalid bundle: 'artifacts' must contain at least one artifact");
257
+ }
258
+
259
+ const seenRoles = new Set<string>();
260
+ const seenFiles = new Set<string>();
261
+ const bundleMajor = majorVersion("euconform.bundle.v1");
262
+ let hasRequiredReport = false;
263
+
264
+ for (let index = 0; index < artifacts.length; index++) {
265
+ const raw = assertObject(artifacts[index], "bundle artifact");
266
+ const fields = readBundleArtifactFields(raw, index);
267
+ assertBundleArtifactIntegrity(fields, seenRoles, seenFiles, bundleMajor);
268
+
269
+ seenRoles.add(fields.role);
270
+ seenFiles.add(fields.fileName);
271
+
272
+ if (fields.role === "report") {
273
+ hasRequiredReport = fields.required;
274
+ }
275
+ }
276
+
277
+ if (!hasRequiredReport) {
278
+ throw new Error("Invalid bundle: a required 'report' artifact is mandatory");
279
+ }
280
+
281
+ return data as ScanBundle;
282
+ }
283
+
284
+ export function validateEcefJsonDocument(
285
+ schemaVersion: string,
286
+ data: unknown
287
+ ): ScanReport | AiBillOfMaterials | CiReport | ScanBundle {
288
+ switch (schemaVersion) {
289
+ case "euconform.report.v1":
290
+ return validateScanReport(data);
291
+ case "euconform.aibom.v1":
292
+ return validateAiBillOfMaterials(data);
293
+ case "euconform.ci.v1":
294
+ return validateCiReport(data);
295
+ case "euconform.bundle.v1":
296
+ return validateScanBundle(data);
297
+ default:
298
+ throw new Error(`Unsupported EuConform Evidence Format schemaVersion '${schemaVersion}'`);
299
+ }
300
+ }
package/src/index.ts CHANGED
@@ -147,6 +147,7 @@ export {
147
147
  type UserPreferences,
148
148
  type EnhancedModelSelectionState,
149
149
  type CapabilityDetectionStatus,
150
+ type CapabilityCache,
150
151
  } from "./inference";
151
152
 
152
153
  // Inference Factory
@@ -132,7 +132,12 @@ export async function createAutoInferenceClient(preferredModel?: string): Promis
132
132
  }
133
133
 
134
134
  // Re-export types and utilities
135
- export { detectOllama, listOllamaModels, OllamaClient } from "./ollama-client";
135
+ export {
136
+ detectOllama,
137
+ listOllamaModels,
138
+ OllamaClient,
139
+ type CapabilityCache,
140
+ } from "./ollama-client";
136
141
  export { BrowserInference, checkWebGPU, BROWSER_MODELS } from "./browser-inference";
137
142
  export {
138
143
  InferenceFactory,