@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,235 @@
1
+ /**
2
+ * File discovery for the EuConform scanner.
3
+ *
4
+ * Walks a target directory, respects .gitignore, filters by extension
5
+ * and named files, and returns file contents for pattern matching.
6
+ */
7
+ import { readFile, readdir, stat } from "node:fs/promises";
8
+ import { basename, extname, join, resolve } from "node:path";
9
+ import ignore from "ignore";
10
+ const DEFAULT_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".json", ".md", ".yaml", ".yml"];
11
+ /** Named files to always include regardless of extension */
12
+ const NAMED_FILE_PATTERNS = [
13
+ /^package\.json$/,
14
+ /^pnpm-lock\.yaml$/,
15
+ /^package-lock\.json$/,
16
+ /^yarn\.lock$/,
17
+ /^\.env\.example$/,
18
+ /^\.env\.sample$/,
19
+ /^next\.config\./,
20
+ /^vite\.config\./,
21
+ /^docker-compose/,
22
+ /^Dockerfile/,
23
+ /^tsconfig.*\.json$/,
24
+ ];
25
+ const ALWAYS_SKIP_DIRS = [
26
+ "node_modules",
27
+ ".git",
28
+ "dist",
29
+ ".next",
30
+ "build",
31
+ "coverage",
32
+ ".turbo",
33
+ ".cache",
34
+ ".euconform",
35
+ ];
36
+ const DEFAULT_MAX_FILE_SIZE = 1_048_576; // 1 MB
37
+ const DEFAULT_MAX_FILES = 10_000;
38
+ const DEFAULT_SCOPE = "production";
39
+ const PRODUCTION_SKIP_PROVENANCE = new Set([
40
+ "test",
41
+ "fixture",
42
+ "example",
43
+ "generated",
44
+ ]);
45
+ const TEST_SEGMENTS = new Set(["test", "tests", "__test__", "__tests__", "spec", "__spec__"]);
46
+ const FIXTURE_SEGMENTS = new Set([
47
+ "fixture",
48
+ "fixtures",
49
+ "__fixture__",
50
+ "__fixtures__",
51
+ "mocks",
52
+ "__mocks__",
53
+ "mock-data",
54
+ ]);
55
+ const EXAMPLE_SEGMENTS = new Set(["example", "examples", "sample", "samples", "demo", "demos"]);
56
+ const GENERATED_SEGMENTS = new Set(["generated", "__generated__", ".generated", "gen"]);
57
+ const TOOLING_SEGMENTS = new Set([".github", "scripts", "tools", "tooling", "bin"]);
58
+ const TEST_FILE_PATTERN = /(?:^|\.)(?:test|spec)\.[^.]+$/i;
59
+ const GENERATED_FILE_PATTERN = /(?:^|\.)(?:generated|min)\.[^.]+$/i;
60
+ const BINARY_EXTENSIONS = new Set([
61
+ ".png",
62
+ ".jpg",
63
+ ".jpeg",
64
+ ".gif",
65
+ ".ico",
66
+ ".svg",
67
+ ".woff",
68
+ ".woff2",
69
+ ".ttf",
70
+ ".eot",
71
+ ".zip",
72
+ ".tar",
73
+ ".gz",
74
+ ".br",
75
+ ".pdf",
76
+ ".doc",
77
+ ".docx",
78
+ ".mp3",
79
+ ".mp4",
80
+ ".webm",
81
+ ".ogg",
82
+ ".wasm",
83
+ ".node",
84
+ ".dylib",
85
+ ".so",
86
+ ".lock", // bun.lockb is binary
87
+ ]);
88
+ function isBinaryExtension(ext) {
89
+ return BINARY_EXTENSIONS.has(ext);
90
+ }
91
+ function isNamedFile(fileName) {
92
+ return NAMED_FILE_PATTERNS.some((pattern) => pattern.test(fileName));
93
+ }
94
+ function shouldIncludeFile(fileName, ext, extensions) {
95
+ if (isNamedFile(fileName))
96
+ return true;
97
+ if (isBinaryExtension(ext))
98
+ return false;
99
+ return extensions.includes(ext);
100
+ }
101
+ export function classifyFileProvenance(relativePath) {
102
+ const normalized = relativePath.replace(/\\/g, "/");
103
+ const rawSegments = normalized.split("/").filter(Boolean);
104
+ const segments = rawSegments.map((segment) => segment.toLowerCase());
105
+ const rawFileName = rawSegments[rawSegments.length - 1] ?? "";
106
+ const fileName = segments[segments.length - 1] ?? "";
107
+ const ext = extname(fileName);
108
+ const isRootFile = segments.length === 1;
109
+ if (segments.some((segment) => FIXTURE_SEGMENTS.has(segment)))
110
+ return "fixture";
111
+ if (segments.some((segment) => TEST_SEGMENTS.has(segment)) || TEST_FILE_PATTERN.test(fileName)) {
112
+ return "test";
113
+ }
114
+ if (segments.some((segment) => EXAMPLE_SEGMENTS.has(segment)))
115
+ return "example";
116
+ if (segments.some((segment) => GENERATED_SEGMENTS.has(segment)) ||
117
+ GENERATED_FILE_PATTERN.test(fileName)) {
118
+ return "generated";
119
+ }
120
+ if (segments.some((segment) => TOOLING_SEGMENTS.has(segment)))
121
+ return "tooling";
122
+ if (fileName.startsWith("readme") && isRootFile)
123
+ return "root-docs";
124
+ if (ext === ".md")
125
+ return "docs";
126
+ if (isNamedFile(rawFileName) ||
127
+ ext === ".yaml" ||
128
+ ext === ".yml" ||
129
+ fileName.startsWith(".env") ||
130
+ /\.config\.[^.]+$/i.test(rawFileName)) {
131
+ return "config";
132
+ }
133
+ return "runtime";
134
+ }
135
+ async function loadGitignore(targetPath) {
136
+ const ig = ignore();
137
+ try {
138
+ const content = await readFile(join(targetPath, ".gitignore"), "utf-8");
139
+ ig.add(content);
140
+ }
141
+ catch {
142
+ // no .gitignore — that's fine
143
+ }
144
+ // Always ignore these regardless
145
+ ig.add(ALWAYS_SKIP_DIRS.map((d) => `${d}/`));
146
+ return ig;
147
+ }
148
+ function shouldProcessEntry(entry, targetPath, filterOpts) {
149
+ const absolutePath = join(targetPath, entry);
150
+ const fileName = basename(entry);
151
+ const ext = extname(fileName).toLowerCase();
152
+ const provenance = classifyFileProvenance(entry);
153
+ if (filterOpts.scope === "production" && PRODUCTION_SKIP_PROVENANCE.has(provenance)) {
154
+ return "skip";
155
+ }
156
+ if (!shouldIncludeFile(fileName, ext, filterOpts.extensions))
157
+ return "ignore";
158
+ return { absolutePath, ext, provenance };
159
+ }
160
+ async function tryReadFile(absolutePath, entry, ext, provenance, maxSize) {
161
+ try {
162
+ const fileStat = await stat(absolutePath);
163
+ if (!fileStat.isFile() || fileStat.size === 0)
164
+ return "ignore";
165
+ if (fileStat.size > maxSize)
166
+ return "skip";
167
+ const content = await readFile(absolutePath, "utf-8");
168
+ return {
169
+ relativePath: entry,
170
+ absolutePath,
171
+ extension: ext,
172
+ content,
173
+ sizeBytes: fileStat.size,
174
+ provenance,
175
+ };
176
+ }
177
+ catch {
178
+ return "skip";
179
+ }
180
+ }
181
+ function resolveOptions(options) {
182
+ return {
183
+ targetPath: resolve(options.targetPath),
184
+ scope: options.scope ?? DEFAULT_SCOPE,
185
+ extensions: options.extensions ?? DEFAULT_EXTENSIONS,
186
+ maxSize: options.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE,
187
+ maxFiles: options.maxFiles ?? DEFAULT_MAX_FILES,
188
+ excludeGlobs: options.excludeGlobs,
189
+ };
190
+ }
191
+ async function readDirectoryEntries(targetPath) {
192
+ try {
193
+ return (await readdir(targetPath, { recursive: true, encoding: "utf-8" }));
194
+ }
195
+ catch (err) {
196
+ throw new Error(`Cannot read directory: ${targetPath}: ${err}`);
197
+ }
198
+ }
199
+ function processEntry(entry, ig, targetPath, filterOpts) {
200
+ if (ig.ignores(entry))
201
+ return "ignore";
202
+ return shouldProcessEntry(entry, targetPath, filterOpts);
203
+ }
204
+ export async function discoverFiles(options) {
205
+ const { targetPath, scope, extensions, maxSize, maxFiles, excludeGlobs } = resolveOptions(options);
206
+ const ig = await loadGitignore(targetPath);
207
+ if (excludeGlobs?.length)
208
+ ig.add(excludeGlobs);
209
+ const entries = await readDirectoryEntries(targetPath);
210
+ const filterOpts = { scope, extensions, maxSize };
211
+ const files = [];
212
+ let skippedCount = 0;
213
+ for (const entry of entries) {
214
+ if (files.length >= maxFiles) {
215
+ skippedCount++;
216
+ continue;
217
+ }
218
+ const processed = processEntry(entry, ig, targetPath, filterOpts);
219
+ if (processed === "skip") {
220
+ skippedCount++;
221
+ continue;
222
+ }
223
+ if (processed === "ignore")
224
+ continue;
225
+ const result = await tryReadFile(processed.absolutePath, entry, processed.ext, processed.provenance, maxSize);
226
+ if (result === "skip") {
227
+ skippedCount++;
228
+ continue;
229
+ }
230
+ if (result === "ignore")
231
+ continue;
232
+ files.push(result);
233
+ }
234
+ return { files, skippedCount };
235
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @euconform/core/scanner — Local repository scanner
3
+ *
4
+ * Scans a project directory for AI components, compliance signals,
5
+ * and regulatory hints. Produces evidence, not verdicts.
6
+ */
7
+ export { scanRepository } from "./aggregator";
8
+ export { discoverFiles } from "./discovery";
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/scanner/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @euconform/core/scanner — Local repository scanner
3
+ *
4
+ * Scans a project directory for AI components, compliance signals,
5
+ * and regulatory hints. Produces evidence, not verdicts.
6
+ */
7
+ export { scanRepository } from "./aggregator";
8
+ export { discoverFiles } from "./discovery";
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@euconform/core",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "license": "MIT OR EUPL-1.2",
5
5
  "author": "Benedikt Hiepler",
6
6
  "description": "Core engine for EU AI Act compliance, risk classification, and fairness metrics.",
7
7
  "type": "module",
8
+ "sideEffects": false,
8
9
  "keywords": [
9
10
  "eu-ai-act",
10
11
  "compliance",
@@ -26,7 +27,10 @@
26
27
  ".": "./src/index.ts",
27
28
  "./risk-engine": "./src/risk-engine.ts",
28
29
  "./fairness-metrics": "./src/fairness-metrics.ts",
29
- "./types": "./src/types.ts"
30
+ "./types": "./src/types.ts",
31
+ "./scanner": "./src/scanner/index.ts",
32
+ "./evidence": "./src/evidence/index.ts",
33
+ "./datasets": "./src/datasets/crows-pairs.ts"
30
34
  },
31
35
  "devDependencies": {
32
36
  "@types/node": "^22.15.30",
@@ -36,7 +40,8 @@
36
40
  "@euconform/typescript-config": "0.0.0"
37
41
  },
38
42
  "dependencies": {
39
- "@xenova/transformers": "^2.17.2"
43
+ "@xenova/transformers": "^2.17.2",
44
+ "ignore": "^7.0.3"
40
45
  },
41
46
  "scripts": {
42
47
  "build": "tsc",
@@ -0,0 +1,64 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import type { CrowsPairsEntry } from "../types";
5
+
6
+ interface CrowsPairsDataset {
7
+ metadata: {
8
+ name: string;
9
+ description: string;
10
+ version: string;
11
+ license: string;
12
+ source: string;
13
+ };
14
+ pairs: Array<{
15
+ id: number;
16
+ stereotype: string;
17
+ antiStereotype: string;
18
+ biasType: string;
19
+ }>;
20
+ }
21
+
22
+ function findDatasetPath(language: "en" | "de"): string {
23
+ const fileName = `crows-pairs-${language}.json`;
24
+
25
+ // Strategy 1: Relative to this source file (works in unbundled tsc builds)
26
+ const currentDir = dirname(fileURLToPath(import.meta.url));
27
+ const fromSource = resolve(currentDir, `../../assets/datasets/${fileName}`);
28
+ if (existsSync(fromSource)) return fromSource;
29
+
30
+ // Strategy 2: Relative to process.cwd() (works in monorepo context)
31
+ const fromCwd = resolve(process.cwd(), `packages/core/assets/datasets/${fileName}`);
32
+ if (existsSync(fromCwd)) return fromCwd;
33
+
34
+ // Strategy 3: Walk up from the entry script to find the monorepo root
35
+ if (process.argv[1]) {
36
+ let dir = dirname(resolve(process.argv[1]));
37
+ for (let i = 0; i < 5; i++) {
38
+ const candidate = resolve(dir, `packages/core/assets/datasets/${fileName}`);
39
+ if (existsSync(candidate)) return candidate;
40
+ dir = dirname(dir);
41
+ }
42
+ }
43
+
44
+ throw new Error(
45
+ `CrowS-Pairs dataset (${language}) not found. Ensure packages/core/assets/datasets/ exists.`
46
+ );
47
+ }
48
+
49
+ /**
50
+ * Load CrowS-Pairs dataset from bundled assets.
51
+ * Converts the structured JSON format to CrowsPairsEntry[] for the bias engine.
52
+ */
53
+ export function loadCrowsPairsDataset(language: "en" | "de"): CrowsPairsEntry[] {
54
+ const filePath = findDatasetPath(language);
55
+ const raw = readFileSync(filePath, "utf-8");
56
+ const dataset: CrowsPairsDataset = JSON.parse(raw);
57
+
58
+ return dataset.pairs.map((pair) => ({
59
+ id: pair.id,
60
+ sent_more: pair.stereotype,
61
+ sent_less: pair.antiStereotype,
62
+ bias_type: pair.biasType,
63
+ }));
64
+ }
@@ -0,0 +1,65 @@
1
+ import { sha256Hex } from "./hash";
2
+ import type { BundleArtifactRef, BundleArtifactRole, ScanBundle } from "./types";
3
+
4
+ interface ArtifactInput {
5
+ content: string;
6
+ fileName: string;
7
+ }
8
+
9
+ interface BuildBundleOptions {
10
+ report: ArtifactInput;
11
+ aibom?: ArtifactInput;
12
+ ci?: ArtifactInput;
13
+ summary?: ArtifactInput;
14
+ tool: { name: string; version: string };
15
+ target: { name: string; rootPath: string };
16
+ generatedAt: string;
17
+ }
18
+
19
+ function extractSchemaVersion(content: string): string | undefined {
20
+ try {
21
+ const parsed = JSON.parse(content) as { schemaVersion?: string };
22
+ return typeof parsed.schemaVersion === "string" ? parsed.schemaVersion : undefined;
23
+ } catch {
24
+ return undefined;
25
+ }
26
+ }
27
+
28
+ function makeArtifactRef(
29
+ role: BundleArtifactRole,
30
+ input: ArtifactInput,
31
+ required: boolean
32
+ ): BundleArtifactRef {
33
+ const isJson = input.fileName.endsWith(".json");
34
+ const ref: BundleArtifactRef = {
35
+ role,
36
+ fileName: input.fileName,
37
+ sha256: sha256Hex(input.content),
38
+ required,
39
+ };
40
+
41
+ if (isJson) {
42
+ const sv = extractSchemaVersion(input.content);
43
+ if (sv) ref.schemaVersion = sv;
44
+ } else {
45
+ ref.mimeType = "text/markdown";
46
+ }
47
+
48
+ return ref;
49
+ }
50
+
51
+ export function buildBundleManifest(opts: BuildBundleOptions): ScanBundle {
52
+ const artifacts: BundleArtifactRef[] = [makeArtifactRef("report", opts.report, true)];
53
+
54
+ if (opts.aibom) artifacts.push(makeArtifactRef("aibom", opts.aibom, false));
55
+ if (opts.ci) artifacts.push(makeArtifactRef("ci", opts.ci, false));
56
+ if (opts.summary) artifacts.push(makeArtifactRef("summary", opts.summary, false));
57
+
58
+ return {
59
+ schemaVersion: "euconform.bundle.v1",
60
+ generatedAt: opts.generatedAt,
61
+ tool: opts.tool,
62
+ target: opts.target,
63
+ artifacts,
64
+ };
65
+ }
@@ -0,0 +1,114 @@
1
+ import type { DetectedSignal, FileProvenance, SignalCategory, SignalEvidence } from "./types";
2
+
3
+ export const IMPLEMENTATION_PROVENANCE = new Set<FileProvenance>(["runtime", "config"]);
4
+ const SUPPORTING_PROVENANCE = new Set<FileProvenance>(["root-docs", "docs"]);
5
+ const USER_FACING_SEGMENTS = new Set([
6
+ "app",
7
+ "pages",
8
+ "routes",
9
+ "route",
10
+ "components",
11
+ "ui",
12
+ "views",
13
+ "view",
14
+ "screens",
15
+ "screen",
16
+ "templates",
17
+ "template",
18
+ "public",
19
+ "content",
20
+ "locales",
21
+ "messages",
22
+ "copy",
23
+ "emails",
24
+ ]);
25
+ const USER_FACING_BASENAME_PATTERN =
26
+ /(?:page|layout|route|template|loading|error|view|screen|dialog|modal|form|widget|banner|notice)\.[^.]+$/i;
27
+
28
+ function normalizedProvenance(provenance?: FileProvenance): FileProvenance {
29
+ return provenance ?? "runtime";
30
+ }
31
+
32
+ function isInternalAnalysisPath(relativePath: string): boolean {
33
+ const normalized = relativePath.replace(/\\/g, "/").toLowerCase();
34
+ return normalized.includes("/scanner/") || normalized.includes("/detectors/");
35
+ }
36
+
37
+ export function isUserFacingPath(relativePath: string): boolean {
38
+ const normalized = relativePath.replace(/\\/g, "/").toLowerCase();
39
+ const segments = normalized.split("/").filter(Boolean);
40
+ const basename = segments[segments.length - 1] ?? "";
41
+
42
+ return (
43
+ segments.some((segment) => USER_FACING_SEGMENTS.has(segment)) ||
44
+ USER_FACING_BASENAME_PATTERN.test(basename)
45
+ );
46
+ }
47
+
48
+ export function isImplementationEvidence(
49
+ category: SignalCategory,
50
+ evidence: SignalEvidence
51
+ ): boolean {
52
+ const provenance = normalizedProvenance(evidence.provenance);
53
+ if (!IMPLEMENTATION_PROVENANCE.has(provenance)) return false;
54
+ if (isInternalAnalysisPath(evidence.file)) return false;
55
+
56
+ if (category === "compliance-disclosure") {
57
+ return isUserFacingPath(evidence.file);
58
+ }
59
+
60
+ return true;
61
+ }
62
+
63
+ export function isSupportingEvidence(category: SignalCategory, evidence: SignalEvidence): boolean {
64
+ const provenance = normalizedProvenance(evidence.provenance);
65
+ if (!SUPPORTING_PROVENANCE.has(provenance)) return false;
66
+
67
+ if (category === "compliance-disclosure") return true;
68
+ if (category.startsWith("compliance-")) return true;
69
+
70
+ return false;
71
+ }
72
+
73
+ function withFilteredEvidence(
74
+ signal: DetectedSignal,
75
+ matcher: (evidence: SignalEvidence) => boolean
76
+ ): DetectedSignal | null {
77
+ const evidence = signal.evidence.filter(matcher);
78
+ if (evidence.length === 0) return null;
79
+ return { ...signal, evidence };
80
+ }
81
+
82
+ export interface EvaluatedSignalSet {
83
+ implementationSignals: DetectedSignal[];
84
+ supportingSignals: DetectedSignal[];
85
+ }
86
+
87
+ export function evaluateSignalsForCategories(
88
+ signals: DetectedSignal[],
89
+ categories: SignalCategory[]
90
+ ): EvaluatedSignalSet {
91
+ const relevant = signals.filter((signal) => categories.includes(signal.category));
92
+
93
+ return {
94
+ implementationSignals: relevant
95
+ .map((signal) =>
96
+ withFilteredEvidence(signal, (evidence) =>
97
+ isImplementationEvidence(signal.category, evidence)
98
+ )
99
+ )
100
+ .filter((signal): signal is DetectedSignal => signal !== null),
101
+ supportingSignals: relevant
102
+ .map((signal) =>
103
+ withFilteredEvidence(signal, (evidence) => isSupportingEvidence(signal.category, evidence))
104
+ )
105
+ .filter((signal): signal is DetectedSignal => signal !== null),
106
+ };
107
+ }
108
+
109
+ export function hasImplementationSignals(
110
+ signals: DetectedSignal[],
111
+ categories: SignalCategory[]
112
+ ): boolean {
113
+ return evaluateSignalsForCategories(signals, categories).implementationSignals.length > 0;
114
+ }
@@ -0,0 +1,5 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ export function sha256Hex(content: string): string {
4
+ return createHash("sha256").update(content, "utf8").digest("hex");
5
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @euconform/core/evidence — Evidence model and output generation
3
+ *
4
+ * This module defines the evidence types produced by the scanner
5
+ * and transforms raw scan results into structured output artifacts.
6
+ */
7
+
8
+ export type {
9
+ AiBillOfMaterials,
10
+ AppType,
11
+ AssessmentHint,
12
+ BomComponent,
13
+ BomComponentKind,
14
+ BundleArtifactRef,
15
+ BundleArtifactRole,
16
+ CiReport,
17
+ ComplianceSignalGroup,
18
+ ConfidenceLevel,
19
+ DetectedSignal,
20
+ DetectorContext,
21
+ FailOnLevel,
22
+ FileProvenance,
23
+ GapCounts,
24
+ OpenQuestion,
25
+ RepoProfile,
26
+ ScanBundle,
27
+ ScanFile,
28
+ ScanGap,
29
+ ScanMeta,
30
+ ScanOptions,
31
+ ScanOutput,
32
+ ScanReport,
33
+ ScanResult,
34
+ ScanScope,
35
+ SignalCategory,
36
+ SignalDetector,
37
+ SignalEvidence,
38
+ } from "./types";
39
+
40
+ export { generateScanOutput } from "./output";
41
+ export { generateSummaryMarkdown } from "./markdown";
42
+ export { buildBundleManifest } from "./bundle";
43
+ export { sha256Hex } from "./hash";
44
+ export {
45
+ getSchemaMajorVersion,
46
+ validateAiBillOfMaterials,
47
+ validateCiReport,
48
+ validateEcefJsonDocument,
49
+ validateScanBundle,
50
+ validateScanReport,
51
+ } from "./validate";