@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.
Files changed (183) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +12 -0
  3. package/dist/evidence/bundle.d.ts +23 -0
  4. package/dist/evidence/bundle.d.ts.map +1 -0
  5. package/dist/evidence/bundle.js +44 -0
  6. package/dist/evidence/compliance-evaluation.d.ts +12 -0
  7. package/dist/evidence/compliance-evaluation.d.ts.map +1 -0
  8. package/dist/evidence/compliance-evaluation.js +78 -0
  9. package/dist/evidence/hash.d.ts +2 -0
  10. package/dist/evidence/hash.d.ts.map +1 -0
  11. package/dist/evidence/hash.js +4 -0
  12. package/dist/evidence/index.d.ts +13 -0
  13. package/dist/evidence/index.d.ts.map +1 -0
  14. package/dist/evidence/index.js +11 -0
  15. package/dist/evidence/markdown.d.ts +9 -0
  16. package/dist/evidence/markdown.d.ts.map +1 -0
  17. package/dist/evidence/markdown.js +169 -0
  18. package/dist/evidence/output.d.ts +13 -0
  19. package/dist/evidence/output.d.ts.map +1 -0
  20. package/dist/evidence/output.js +547 -0
  21. package/dist/evidence/types.d.ts +230 -0
  22. package/dist/evidence/types.d.ts.map +1 -0
  23. package/dist/evidence/types.js +7 -0
  24. package/dist/evidence/validate.d.ts +8 -0
  25. package/dist/evidence/validate.d.ts.map +1 -0
  26. package/dist/evidence/validate.js +175 -0
  27. package/dist/index.d.ts +2 -2
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +1 -1
  30. package/dist/inference/ollama-client.d.ts +3 -0
  31. package/dist/inference/ollama-client.d.ts.map +1 -1
  32. package/dist/inference/ollama-client.js +69 -71
  33. package/dist/legal-checks/annex-iv-report.d.ts +3 -0
  34. package/dist/legal-checks/annex-iv-report.d.ts.map +1 -1
  35. package/dist/legal-checks/annex-iv-report.js +1 -0
  36. package/dist/legal-checks/deadline-timeline.d.ts +57 -0
  37. package/dist/legal-checks/deadline-timeline.d.ts.map +1 -0
  38. package/dist/legal-checks/deadline-timeline.js +265 -0
  39. package/dist/legal-checks/gap-analysis.d.ts +48 -0
  40. package/dist/legal-checks/gap-analysis.d.ts.map +1 -0
  41. package/dist/legal-checks/gap-analysis.js +397 -0
  42. package/dist/legal-checks/gpai-classifier.d.ts +49 -0
  43. package/dist/legal-checks/gpai-classifier.d.ts.map +1 -0
  44. package/dist/legal-checks/gpai-classifier.js +197 -0
  45. package/dist/legal-checks/index.d.ts +6 -0
  46. package/dist/legal-checks/index.d.ts.map +1 -1
  47. package/dist/legal-checks/index.js +3 -0
  48. package/dist/scanner/aggregator.d.ts +7 -0
  49. package/dist/scanner/aggregator.d.ts.map +1 -0
  50. package/dist/scanner/aggregator.js +200 -0
  51. package/dist/scanner/detectors/ai-framework-detector.d.ts +3 -0
  52. package/dist/scanner/detectors/ai-framework-detector.d.ts.map +1 -0
  53. package/dist/scanner/detectors/ai-framework-detector.js +74 -0
  54. package/dist/scanner/detectors/ai-provider-detector.d.ts +3 -0
  55. package/dist/scanner/detectors/ai-provider-detector.d.ts.map +1 -0
  56. package/dist/scanner/detectors/ai-provider-detector.js +100 -0
  57. package/dist/scanner/detectors/compliance-detector.d.ts +3 -0
  58. package/dist/scanner/detectors/compliance-detector.d.ts.map +1 -0
  59. package/dist/scanner/detectors/compliance-detector.js +201 -0
  60. package/dist/scanner/detectors/config-detector.d.ts +3 -0
  61. package/dist/scanner/detectors/config-detector.d.ts.map +1 -0
  62. package/dist/scanner/detectors/config-detector.js +248 -0
  63. package/dist/scanner/detectors/framework-detector.d.ts +3 -0
  64. package/dist/scanner/detectors/framework-detector.d.ts.map +1 -0
  65. package/dist/scanner/detectors/framework-detector.js +141 -0
  66. package/dist/scanner/detectors/index.d.ts +8 -0
  67. package/dist/scanner/detectors/index.d.ts.map +1 -0
  68. package/dist/scanner/detectors/index.js +37 -0
  69. package/dist/scanner/detectors/local-inference-detector.d.ts +3 -0
  70. package/dist/scanner/detectors/local-inference-detector.d.ts.map +1 -0
  71. package/dist/scanner/detectors/local-inference-detector.js +93 -0
  72. package/dist/scanner/detectors/markdown-detector.d.ts +3 -0
  73. package/dist/scanner/detectors/markdown-detector.d.ts.map +1 -0
  74. package/dist/scanner/detectors/markdown-detector.js +139 -0
  75. package/dist/scanner/detectors/package-json-detector.d.ts +3 -0
  76. package/dist/scanner/detectors/package-json-detector.d.ts.map +1 -0
  77. package/dist/scanner/detectors/package-json-detector.js +166 -0
  78. package/dist/scanner/detectors/rag-detector.d.ts +3 -0
  79. package/dist/scanner/detectors/rag-detector.d.ts.map +1 -0
  80. package/dist/scanner/detectors/rag-detector.js +132 -0
  81. package/dist/scanner/detectors/shared.d.ts +18 -0
  82. package/dist/scanner/detectors/shared.d.ts.map +1 -0
  83. package/dist/scanner/detectors/shared.js +36 -0
  84. package/dist/scanner/discovery.d.ts +14 -0
  85. package/dist/scanner/discovery.d.ts.map +1 -0
  86. package/dist/scanner/discovery.js +235 -0
  87. package/dist/scanner/index.d.ts +9 -0
  88. package/dist/scanner/index.d.ts.map +1 -0
  89. package/dist/scanner/index.js +8 -0
  90. package/package.json +6 -3
  91. package/src/evidence/bundle.ts +65 -0
  92. package/src/evidence/compliance-evaluation.ts +114 -0
  93. package/src/evidence/hash.ts +5 -0
  94. package/src/evidence/index.ts +51 -0
  95. package/src/evidence/markdown.ts +226 -0
  96. package/src/evidence/output.ts +717 -0
  97. package/src/evidence/types.ts +329 -0
  98. package/src/evidence/validate.ts +270 -0
  99. package/src/index.ts +25 -0
  100. package/src/inference/ollama-client.ts +93 -86
  101. package/src/legal-checks/annex-iv-report.ts +4 -0
  102. package/src/legal-checks/deadline-timeline.ts +313 -0
  103. package/src/legal-checks/gap-analysis.ts +454 -0
  104. package/src/legal-checks/gpai-classifier.ts +281 -0
  105. package/src/legal-checks/index.ts +33 -0
  106. package/src/scanner/aggregator.ts +258 -0
  107. package/src/scanner/detectors/ai-framework-detector.ts +94 -0
  108. package/src/scanner/detectors/ai-provider-detector.ts +135 -0
  109. package/src/scanner/detectors/compliance-detector.ts +232 -0
  110. package/src/scanner/detectors/config-detector.ts +316 -0
  111. package/src/scanner/detectors/framework-detector.ts +163 -0
  112. package/src/scanner/detectors/index.ts +40 -0
  113. package/src/scanner/detectors/local-inference-detector.ts +124 -0
  114. package/src/scanner/detectors/markdown-detector.ts +176 -0
  115. package/src/scanner/detectors/package-json-detector.ts +202 -0
  116. package/src/scanner/detectors/rag-detector.ts +162 -0
  117. package/src/scanner/detectors/shared.ts +45 -0
  118. package/src/scanner/discovery.ts +290 -0
  119. package/src/scanner/index.ts +9 -0
  120. package/tests/evidence/bundle.test.ts +128 -0
  121. package/tests/evidence/ecef-schemas.test.ts +316 -0
  122. package/tests/evidence/hash.test.ts +25 -0
  123. package/tests/evidence/markdown.test.ts +92 -0
  124. package/tests/evidence/output.test.ts +203 -0
  125. package/tests/fixtures/compliance-good/README.md +14 -0
  126. package/tests/fixtures/compliance-good/lib/ai-service.ts +17 -0
  127. package/tests/fixtures/compliance-good/lib/review-flow.ts +11 -0
  128. package/tests/fixtures/compliance-good/package.json +10 -0
  129. package/tests/fixtures/compliance-good/tests/bias-check.test.ts +13 -0
  130. package/tests/fixtures/nextjs-openai/README.md +5 -0
  131. package/tests/fixtures/nextjs-openai/app/page.tsx +11 -0
  132. package/tests/fixtures/nextjs-openai/package.json +9 -0
  133. package/tests/fixtures/ollama-rag/.env.example +2 -0
  134. package/tests/fixtures/ollama-rag/docker-compose.yml +9 -0
  135. package/tests/fixtures/ollama-rag/package.json +9 -0
  136. package/tests/fixtures/ollama-rag/server.ts +21 -0
  137. package/tests/fixtures/plain-webapp/README.md +3 -0
  138. package/tests/fixtures/plain-webapp/index.js +5 -0
  139. package/tests/fixtures/plain-webapp/package.json +8 -0
  140. package/tests/gpai-classifier.test.ts +125 -0
  141. package/tests/gpai-timeline.test.ts +55 -0
  142. package/tests/scanner/aggregator.test.ts +122 -0
  143. package/tests/scanner/detectors.test.ts +274 -0
  144. package/tests/scanner/discovery.test.ts +87 -0
  145. package/vitest.config.ts +1 -0
  146. package/.turbo/turbo-check-types.log +0 -4
  147. package/.turbo/turbo-test.log +0 -364
  148. package/coverage/base.css +0 -224
  149. package/coverage/block-navigation.js +0 -87
  150. package/coverage/clover.xml +0 -3282
  151. package/coverage/coverage-final.json +0 -20
  152. package/coverage/favicon.png +0 -0
  153. package/coverage/index.html +0 -161
  154. package/coverage/prettify.css +0 -1
  155. package/coverage/prettify.js +0 -2
  156. package/coverage/sort-arrow-sprite.png +0 -0
  157. package/coverage/sorter.js +0 -210
  158. package/coverage/src/fairness-metrics.ts.html +0 -1579
  159. package/coverage/src/index.html +0 -161
  160. package/coverage/src/index.ts.html +0 -535
  161. package/coverage/src/inference/browser-inference.ts.html +0 -859
  162. package/coverage/src/inference/capability-detection.ts.html +0 -2227
  163. package/coverage/src/inference/index.html +0 -176
  164. package/coverage/src/inference/index.ts.html +0 -553
  165. package/coverage/src/inference/inference-factory.ts.html +0 -751
  166. package/coverage/src/inference/ollama-client.ts.html +0 -1627
  167. package/coverage/src/legal-checks/annex-iv-report.ts.html +0 -556
  168. package/coverage/src/legal-checks/bias-metrics.ts.html +0 -1855
  169. package/coverage/src/legal-checks/data-governance.ts.html +0 -529
  170. package/coverage/src/legal-checks/human-oversight-logging.ts.html +0 -490
  171. package/coverage/src/legal-checks/index.html +0 -206
  172. package/coverage/src/legal-checks/index.ts.html +0 -250
  173. package/coverage/src/legal-checks/risk-classifier.ts.html +0 -940
  174. package/coverage/src/legal-checks/robustness-transparency.ts.html +0 -571
  175. package/coverage/src/metrics/crows-pairs-bias.ts.html +0 -820
  176. package/coverage/src/metrics/index.html +0 -146
  177. package/coverage/src/metrics/index.ts.html +0 -151
  178. package/coverage/src/metrics/stereotype-bias.ts.html +0 -886
  179. package/coverage/src/risk-engine.ts.html +0 -1660
  180. package/coverage/src/types.ts.html +0 -547
  181. package/dist/legal-checks/disclaimer.d.ts +0 -2
  182. package/dist/legal-checks/disclaimer.d.ts.map +0 -1
  183. package/dist/legal-checks/disclaimer.js +0 -1
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@euconform/core",
3
- "version": "1.0.1",
3
+ "version": "1.3.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.",
@@ -26,7 +26,9 @@
26
26
  ".": "./src/index.ts",
27
27
  "./risk-engine": "./src/risk-engine.ts",
28
28
  "./fairness-metrics": "./src/fairness-metrics.ts",
29
- "./types": "./src/types.ts"
29
+ "./types": "./src/types.ts",
30
+ "./scanner": "./src/scanner/index.ts",
31
+ "./evidence": "./src/evidence/index.ts"
30
32
  },
31
33
  "devDependencies": {
32
34
  "@types/node": "^22.15.30",
@@ -36,7 +38,8 @@
36
38
  "@euconform/typescript-config": "0.0.0"
37
39
  },
38
40
  "dependencies": {
39
- "@xenova/transformers": "^2.17.2"
41
+ "@xenova/transformers": "^2.17.2",
42
+ "ignore": "^7.0.3"
40
43
  },
41
44
  "scripts": {
42
45
  "build": "tsc",
@@ -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";