@euconform/cli 1.1.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/README.md +107 -3
- package/dist/{browser-inference-AWHXG2TB.js → browser-inference-NZ3X27GT.js} +3 -3
- package/dist/chunk-IWTPVCP2.js +9494 -0
- package/dist/chunk-OTCNLJNL.js +498 -0
- package/dist/{chunk-JQTB43B2.js → chunk-W32HNHWT.js} +29 -0
- package/dist/index.js +1482 -1310
- package/dist/{ollama-client-AJAW266L.js → ollama-client-3U7EGHLA.js} +1 -1
- package/dist/schemas/aibom-v1.1.schema.json +92 -0
- package/dist/schemas/aibom-v1.schema.json +76 -0
- package/dist/schemas/bias-v1.schema.json +138 -0
- package/dist/schemas/bias-v2.schema.json +255 -0
- package/dist/schemas/bundle-v1.schema.json +61 -0
- package/dist/schemas/ci-v1.schema.json +91 -0
- package/dist/schemas/external-observation-v1.schema.json +139 -0
- package/dist/schemas/report-v1.schema.json +199 -0
- package/dist/writer-OTMAB5JA.js +11 -0
- package/package.json +4 -2
- package/dist/chunk-AYXSWY3I.js +0 -11
- package/dist/chunk-F6VBKHY4.js +0 -186
- package/dist/chunk-FEBJZMWY.js +0 -175
- package/dist/run-bias-test-S6DIKHIY.js +0 -10
- package/dist/severity-5EZAEKDS.js +0 -7
package/dist/index.js
CHANGED
|
@@ -1,12 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import "./chunk-W32HNHWT.js";
|
|
2
3
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import "./chunk-JQTB43B2.js";
|
|
4
|
+
TransformersInference,
|
|
5
|
+
isConditionalGenerationConfig
|
|
6
|
+
} from "./chunk-OTCNLJNL.js";
|
|
7
7
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
IMPLEMENTATION_PROVENANCE,
|
|
9
|
+
buildBiasStatistics,
|
|
10
|
+
compareBiasRuns,
|
|
11
|
+
formatBiasComparisonMarkdown,
|
|
12
|
+
generateScanOutput,
|
|
13
|
+
hasImplementationSignals,
|
|
14
|
+
isPartialRun,
|
|
15
|
+
isUserFacingPath,
|
|
16
|
+
sha256Hex,
|
|
17
|
+
validate,
|
|
18
|
+
validateAiBillOfMaterials,
|
|
19
|
+
validateBiasArtifact,
|
|
20
|
+
validateBiasReportV2,
|
|
21
|
+
validateCiReport,
|
|
22
|
+
validateScanBundle,
|
|
23
|
+
validateScanReport,
|
|
24
|
+
writeBundleManifest,
|
|
25
|
+
writeOutputFiles,
|
|
26
|
+
writeZipBundle
|
|
27
|
+
} from "./chunk-IWTPVCP2.js";
|
|
10
28
|
import {
|
|
11
29
|
__commonJS,
|
|
12
30
|
__toESM
|
|
@@ -471,44 +489,602 @@ var require_ignore = __commonJS({
|
|
|
471
489
|
});
|
|
472
490
|
|
|
473
491
|
// src/index.ts
|
|
474
|
-
import { defineCommand as
|
|
492
|
+
import { defineCommand as defineCommand7, runMain } from "citty";
|
|
475
493
|
|
|
476
494
|
// src/commands/bias.ts
|
|
477
495
|
import { mkdir, stat, writeFile } from "fs/promises";
|
|
478
|
-
import { resolve } from "path";
|
|
496
|
+
import { resolve as resolve2 } from "path";
|
|
479
497
|
import { defineCommand } from "citty";
|
|
480
|
-
import
|
|
498
|
+
import consola3 from "consola";
|
|
481
499
|
|
|
482
|
-
// src/
|
|
500
|
+
// src/bias/run-bias-test.ts
|
|
501
|
+
import { createRequire } from "module";
|
|
502
|
+
|
|
503
|
+
// ../core/src/metrics/crows-pairs-bias.ts
|
|
504
|
+
function tokenIdsHash(tokenIds) {
|
|
505
|
+
return sha256Hex(JSON.stringify(tokenIds));
|
|
506
|
+
}
|
|
507
|
+
async function calculateCrowsPairsBias(config, provider) {
|
|
508
|
+
const { dataset, model, engine, allowPartial } = config;
|
|
509
|
+
if (dataset.length === 0) {
|
|
510
|
+
throw new Error("Dataset cannot be empty");
|
|
511
|
+
}
|
|
512
|
+
const selectedPairs = dataset.map((entry) => extractSentences(entry)).filter((pair) => pair !== null);
|
|
513
|
+
const { pairResults, failedPairs, scoringProvenance } = await measurePairs(
|
|
514
|
+
selectedPairs,
|
|
515
|
+
provider,
|
|
516
|
+
{ allowPartial: Boolean(allowPartial) }
|
|
517
|
+
);
|
|
518
|
+
const analyzedCount = pairResults.length;
|
|
519
|
+
const failedCount = failedPairs.length;
|
|
520
|
+
const selectedCount = selectedPairs.length;
|
|
521
|
+
if (analyzedCount === 0 || pairResults[0] === void 0) {
|
|
522
|
+
throw new Error("No valid bias scores calculated from dataset");
|
|
523
|
+
}
|
|
524
|
+
const firstPairResult = pairResults[0];
|
|
525
|
+
const biasScores = pairResults.map((p) => p.biasScore);
|
|
526
|
+
const stereotypicalCount = pairResults.filter((p) => p.biasDirection === "stereotype").length;
|
|
527
|
+
const overallBiasScore = biasScores.reduce((sum, score) => sum + score, 0) / biasScores.length;
|
|
528
|
+
const stereotypicalPreference = stereotypicalCount / biasScores.length * 100;
|
|
529
|
+
return {
|
|
530
|
+
score: Number(overallBiasScore.toFixed(6)),
|
|
531
|
+
// Read from the first measured pair's own reported method — never hardcoded. A
|
|
532
|
+
// provider may score in base mode ("sequence_logprob_sum") or Gemma-4 template mode
|
|
533
|
+
// ("conditional_sequence_logprob_sum"); hardcoding here silently discarded that fact
|
|
534
|
+
// (Messmotor-Sanierung Task 6 finding, same category of bug as the once-hardcoded dtype).
|
|
535
|
+
method: firstPairResult.method,
|
|
536
|
+
pairsAnalyzed: analyzedCount,
|
|
537
|
+
stereotypicalPreference: Number(stereotypicalPreference.toFixed(2)),
|
|
538
|
+
pairResults,
|
|
539
|
+
byBiasType: aggregateByBiasType(pairResults),
|
|
540
|
+
runStatus: failedCount === 0 ? "complete" : "partial",
|
|
541
|
+
selectedCount,
|
|
542
|
+
analyzedCount,
|
|
543
|
+
failedCount,
|
|
544
|
+
failedPairs,
|
|
545
|
+
metadata: {
|
|
546
|
+
engine,
|
|
547
|
+
model,
|
|
548
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
549
|
+
},
|
|
550
|
+
scoringProvenance
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
async function measurePairs(selectedPairs, provider, options) {
|
|
554
|
+
const pairResults = [];
|
|
555
|
+
const failedPairs = [];
|
|
556
|
+
let scoringProvenance;
|
|
557
|
+
for (const pair of selectedPairs) {
|
|
558
|
+
try {
|
|
559
|
+
const [stereoScored, antiScored] = await Promise.all([
|
|
560
|
+
provider.getScoredLogProb(pair.stereo),
|
|
561
|
+
provider.getScoredLogProb(pair.anti)
|
|
562
|
+
]);
|
|
563
|
+
const logprobStereo = stereoScored.score;
|
|
564
|
+
const logprobAnti = antiScored.score;
|
|
565
|
+
const biasScore = logprobStereo - logprobAnti;
|
|
566
|
+
const stereotypeTokenCount = stereoScored.scoredTokenIds.length;
|
|
567
|
+
const antiStereotypeTokenCount = antiScored.scoredTokenIds.length;
|
|
568
|
+
const method = stereoScored.method ?? antiScored.method ?? "sequence_logprob_sum";
|
|
569
|
+
if (scoringProvenance === void 0) {
|
|
570
|
+
scoringProvenance = {
|
|
571
|
+
dtype: stereoScored.provenance.dtype,
|
|
572
|
+
logitsDtype: stereoScored.provenance.logitsDtype,
|
|
573
|
+
backend: stereoScored.provenance.backend,
|
|
574
|
+
libraryVersion: stereoScored.provenance.libraryVersion,
|
|
575
|
+
templateStrategy: stereoScored.provenance.templateStrategy,
|
|
576
|
+
promptText: stereoScored.provenance.promptText,
|
|
577
|
+
promptSha256: stereoScored.provenance.promptSha256,
|
|
578
|
+
contextTokenHash: stereoScored.contextTokenHash
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
pairResults.push({
|
|
582
|
+
pairId: pair.id,
|
|
583
|
+
stereotype: pair.stereo,
|
|
584
|
+
antiStereotype: pair.anti,
|
|
585
|
+
biasType: pair.biasType,
|
|
586
|
+
stereotypeLogProb: logprobStereo,
|
|
587
|
+
antiStereotypeLogProb: logprobAnti,
|
|
588
|
+
biasScore,
|
|
589
|
+
biasDirection: biasScore > 0 ? "stereotype" : biasScore < 0 ? "anti-stereotype" : "neutral",
|
|
590
|
+
method,
|
|
591
|
+
stereotypeTokenCount,
|
|
592
|
+
antiStereotypeTokenCount,
|
|
593
|
+
tokenCountDelta: stereotypeTokenCount - antiStereotypeTokenCount,
|
|
594
|
+
stereotypeTokenHash: tokenIdsHash(stereoScored.scoredTokenIds),
|
|
595
|
+
antiStereotypeTokenHash: tokenIdsHash(antiScored.scoredTokenIds)
|
|
596
|
+
});
|
|
597
|
+
} catch (error) {
|
|
598
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
599
|
+
if (!options.allowPartial) {
|
|
600
|
+
throw new Error(
|
|
601
|
+
`CrowS-Pairs measurement failed for pair ${pair.id ?? "(no id)"}: ${reason}`
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
failedPairs.push({ pairId: pair.id, reason });
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return { pairResults, failedPairs, scoringProvenance };
|
|
608
|
+
}
|
|
609
|
+
function aggregateByBiasType(pairResults) {
|
|
610
|
+
const groups = /* @__PURE__ */ new Map();
|
|
611
|
+
for (const pair of pairResults) {
|
|
612
|
+
const key = pair.biasType ?? "unspecified";
|
|
613
|
+
const bucket = groups.get(key);
|
|
614
|
+
if (bucket) bucket.push(pair);
|
|
615
|
+
else groups.set(key, [pair]);
|
|
616
|
+
}
|
|
617
|
+
return [...groups.entries()].map(([biasType, pairs]) => {
|
|
618
|
+
const stereotypePreferenceCount = pairs.filter((p) => p.biasDirection === "stereotype").length;
|
|
619
|
+
const mean = pairs.reduce((sum, p) => sum + p.biasScore, 0) / pairs.length;
|
|
620
|
+
return {
|
|
621
|
+
biasType,
|
|
622
|
+
pairsCount: pairs.length,
|
|
623
|
+
stereotypePreferenceCount,
|
|
624
|
+
stereotypePreferencePercent: Number(
|
|
625
|
+
(stereotypePreferenceCount / pairs.length * 100).toFixed(2)
|
|
626
|
+
),
|
|
627
|
+
averageBiasScore: Number(mean.toFixed(6))
|
|
628
|
+
};
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
function extractSentences(entry) {
|
|
632
|
+
const stereo = entry.sent_more || entry.stereotype;
|
|
633
|
+
const anti = entry.sent_less || entry.antiStereotype;
|
|
634
|
+
if (!stereo || !anti) return null;
|
|
635
|
+
const raw = entry;
|
|
636
|
+
return {
|
|
637
|
+
stereo,
|
|
638
|
+
anti,
|
|
639
|
+
id: typeof raw.id === "number" ? raw.id : null,
|
|
640
|
+
biasType: typeof raw.bias_type === "string" ? raw.bias_type : typeof raw.biasType === "string" ? raw.biasType : null
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// ../core/src/datasets/crows-pairs.ts
|
|
645
|
+
import { createHash } from "crypto";
|
|
646
|
+
import { existsSync, readFileSync } from "fs";
|
|
647
|
+
import { dirname, resolve } from "path";
|
|
648
|
+
import { fileURLToPath } from "url";
|
|
649
|
+
function findDatasetPath(language) {
|
|
650
|
+
const fileName = `crows-pairs-${language}.json`;
|
|
651
|
+
const currentDir = dirname(fileURLToPath(import.meta.url));
|
|
652
|
+
const fromSource = resolve(currentDir, `../../assets/datasets/${fileName}`);
|
|
653
|
+
if (existsSync(fromSource)) return fromSource;
|
|
654
|
+
const fromCwd = resolve(process.cwd(), `packages/core/assets/datasets/${fileName}`);
|
|
655
|
+
if (existsSync(fromCwd)) return fromCwd;
|
|
656
|
+
if (process.argv[1]) {
|
|
657
|
+
let dir = dirname(resolve(process.argv[1]));
|
|
658
|
+
for (let i = 0; i < 5; i++) {
|
|
659
|
+
const candidate = resolve(dir, `packages/core/assets/datasets/${fileName}`);
|
|
660
|
+
if (existsSync(candidate)) return candidate;
|
|
661
|
+
dir = dirname(dir);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
throw new Error(
|
|
665
|
+
`CrowS-Pairs dataset (${language}) not found. Ensure packages/core/assets/datasets/ exists.`
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
function loadCrowsPairsDataset(language) {
|
|
669
|
+
const filePath = findDatasetPath(language);
|
|
670
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
671
|
+
const dataset = JSON.parse(raw);
|
|
672
|
+
return dataset.pairs.map((pair) => ({
|
|
673
|
+
id: pair.id,
|
|
674
|
+
sent_more: pair.stereotype,
|
|
675
|
+
sent_less: pair.antiStereotype,
|
|
676
|
+
bias_type: pair.biasType
|
|
677
|
+
}));
|
|
678
|
+
}
|
|
679
|
+
function loadCustomDataset(filePath) {
|
|
680
|
+
const resolved = resolve(filePath);
|
|
681
|
+
if (!existsSync(resolved)) {
|
|
682
|
+
throw new Error(`Fragekatalog nicht gefunden: ${resolved}`);
|
|
683
|
+
}
|
|
684
|
+
const raw = readFileSync(resolved, "utf-8");
|
|
685
|
+
let parsed;
|
|
686
|
+
try {
|
|
687
|
+
parsed = JSON.parse(raw);
|
|
688
|
+
} catch {
|
|
689
|
+
throw new Error(`Fragekatalog ist kein g\xFCltiges JSON: ${resolved}`);
|
|
690
|
+
}
|
|
691
|
+
const doc = parsed;
|
|
692
|
+
if (!Array.isArray(doc.pairs) || doc.pairs.length === 0) {
|
|
693
|
+
throw new Error(`Fragekatalog enth\xE4lt keine Paare: ${resolved}`);
|
|
694
|
+
}
|
|
695
|
+
const pairs = doc.pairs.map((entry, index) => {
|
|
696
|
+
const p = entry;
|
|
697
|
+
const stereotype = typeof p.stereotype === "string" ? p.stereotype : void 0;
|
|
698
|
+
const antiStereotype = typeof p.antiStereotype === "string" ? p.antiStereotype : void 0;
|
|
699
|
+
if (!stereotype || !antiStereotype) {
|
|
700
|
+
throw new Error(
|
|
701
|
+
`Fragekatalog: Paar ${p.id ?? index} braucht 'stereotype' und 'antiStereotype' (${resolved})`
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
return {
|
|
705
|
+
id: typeof p.id === "number" ? p.id : index + 1,
|
|
706
|
+
sent_more: stereotype,
|
|
707
|
+
sent_less: antiStereotype,
|
|
708
|
+
bias_type: typeof p.biasType === "string" ? p.biasType : "unspecified"
|
|
709
|
+
};
|
|
710
|
+
});
|
|
711
|
+
const meta = doc.metadata ?? {};
|
|
712
|
+
return {
|
|
713
|
+
pairs,
|
|
714
|
+
metadata: {
|
|
715
|
+
name: typeof meta.name === "string" ? meta.name : "unbenannter Katalog",
|
|
716
|
+
version: typeof meta.version === "string" ? meta.version : "unversioniert",
|
|
717
|
+
sha256: createHash("sha256").update(raw).digest("hex"),
|
|
718
|
+
// Deliberately NOT the resolved absolute filesystem path: euconform.bias.v2's
|
|
719
|
+
// `catalog` schema (additionalProperties: false) has no `path` field on purpose — an
|
|
720
|
+
// absolute, machine-specific path baked into an evidence artifact would defeat the
|
|
721
|
+
// talk's "you can recompute this" promise the moment the repo is cloned to a
|
|
722
|
+
// different machine (Messmotor-Sanierung Task 6 finding: `validate` rejected a real
|
|
723
|
+
// clinical-catalog run over exactly this field). The catalog's identity is already
|
|
724
|
+
// fully portable via name + version + sha256.
|
|
725
|
+
pairCount: pairs.length
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// src/bias/run-bias-test.ts
|
|
483
731
|
import consola from "consola";
|
|
732
|
+
|
|
733
|
+
// src/bias/resolve-model-revision.ts
|
|
734
|
+
var HF_SHA_PATTERN = /^[0-9a-f]{40}$/i;
|
|
735
|
+
async function resolveModelRevisionSha(modelId, options = {}) {
|
|
736
|
+
const attempts = options.attempts ?? 3;
|
|
737
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
738
|
+
const url = `https://huggingface.co/api/models/${modelId}`;
|
|
739
|
+
let lastError;
|
|
740
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
741
|
+
try {
|
|
742
|
+
const response = await fetchImpl(url, {
|
|
743
|
+
headers: { "Cache-Control": "no-cache", Accept: "application/json" }
|
|
744
|
+
});
|
|
745
|
+
if (!response.ok) {
|
|
746
|
+
throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
|
747
|
+
}
|
|
748
|
+
const body = await response.json();
|
|
749
|
+
const sha = body?.sha;
|
|
750
|
+
if (typeof sha !== "string" || !HF_SHA_PATTERN.test(sha)) {
|
|
751
|
+
throw new Error(
|
|
752
|
+
`response did not contain a valid 40-char commit sha (got: ${JSON.stringify(sha)}); this looks like a malformed or stubbed API response, not the real Hub metadata`
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
return sha;
|
|
756
|
+
} catch (err) {
|
|
757
|
+
lastError = err;
|
|
758
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
759
|
+
console.warn(
|
|
760
|
+
`[bias] revision resolution attempt ${attempt}/${attempts} for "${modelId}" failed: ${message}`
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
const lastMessage = lastError instanceof Error ? lastError.message : String(lastError);
|
|
765
|
+
throw new Error(
|
|
766
|
+
`Could not resolve an immutable commit sha for "${modelId}" from the Hugging Face Hub API after ${attempts} attempts. Aborting: measuring against the mutable "main" ref would make provenance.modelRevision meaningless for reproduction. Last error: ${lastMessage}`
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// src/bias/select-pairs.ts
|
|
771
|
+
var SEED_RANGE = 4294967296;
|
|
772
|
+
function mulberry32(seed) {
|
|
773
|
+
let a = seed >>> 0;
|
|
774
|
+
return () => {
|
|
775
|
+
a = a + 1831565813 >>> 0;
|
|
776
|
+
let t = a;
|
|
777
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
778
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
779
|
+
return ((t ^ t >>> 14) >>> 0) / SEED_RANGE;
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
function selectPairs(dataset, maxPairs, seed) {
|
|
783
|
+
if (maxPairs === void 0 || maxPairs >= dataset.length) {
|
|
784
|
+
return { pairs: dataset, seed: null };
|
|
785
|
+
}
|
|
786
|
+
const resolvedSeed = seed ?? Math.floor(Math.random() * SEED_RANGE);
|
|
787
|
+
const rng = mulberry32(resolvedSeed);
|
|
788
|
+
const shuffled = [...dataset];
|
|
789
|
+
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
790
|
+
const j = Math.floor(rng() * (i + 1));
|
|
791
|
+
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
792
|
+
}
|
|
793
|
+
return { pairs: shuffled.slice(0, maxPairs), seed: resolvedSeed };
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// src/bias/run-bias-test.ts
|
|
797
|
+
function resolveTransformersPackageVersion() {
|
|
798
|
+
try {
|
|
799
|
+
const require2 = createRequire(import.meta.url);
|
|
800
|
+
const mod = require2("@huggingface/transformers");
|
|
801
|
+
return mod.env?.version ?? "unknown";
|
|
802
|
+
} catch {
|
|
803
|
+
return "unknown";
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
function resolveOnnxRuntimeVersion() {
|
|
807
|
+
const require2 = createRequire(import.meta.url);
|
|
808
|
+
const pkg = require2("onnxruntime-node/package.json");
|
|
809
|
+
if (!pkg.version) {
|
|
810
|
+
throw new Error(
|
|
811
|
+
"runBiasTest: could not resolve the installed onnxruntime-node version from onnxruntime-node/package.json (missing 'version' field). This is a required, ergebnisrelevant provenance field (Messmotor-Sanierung Task 4) \u2014 refusing to write an artifact with a fabricated or missing value."
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
return pkg.version;
|
|
815
|
+
}
|
|
816
|
+
function pairIdOf(entry) {
|
|
817
|
+
const id = entry?.id;
|
|
818
|
+
return typeof id === "number" ? id : null;
|
|
819
|
+
}
|
|
820
|
+
async function resolveDtype(model, revision) {
|
|
821
|
+
const transformers = await import("@huggingface/transformers");
|
|
822
|
+
const config = await transformers.AutoConfig.from_pretrained(model, { revision });
|
|
823
|
+
const isTemplateModel = isConditionalGenerationConfig(config);
|
|
824
|
+
return isTemplateModel ? "q4" : "fp32";
|
|
825
|
+
}
|
|
826
|
+
async function runBiasTest(options) {
|
|
827
|
+
const { model, datasetSource, maxPairs, seed, allowPartial } = options;
|
|
828
|
+
consola.start(`Resolving immutable revision for '${model}'...`);
|
|
829
|
+
const modelRevision = await resolveModelRevisionSha(model);
|
|
830
|
+
const dtype = await resolveDtype(model, modelRevision);
|
|
831
|
+
const provider = new TransformersInference(
|
|
832
|
+
model,
|
|
833
|
+
(status) => {
|
|
834
|
+
consola.info(`[${model}] ${status}`);
|
|
835
|
+
},
|
|
836
|
+
{ revision: modelRevision, dtype }
|
|
837
|
+
);
|
|
838
|
+
const baseProvenance = {
|
|
839
|
+
engine: "transformers",
|
|
840
|
+
modelId: model,
|
|
841
|
+
modelRevision,
|
|
842
|
+
tokenizerId: model,
|
|
843
|
+
transformersVersion: resolveTransformersPackageVersion(),
|
|
844
|
+
onnxruntimeVersion: resolveOnnxRuntimeVersion(),
|
|
845
|
+
// Read from the provider actually constructed above, not a second hardcoded literal —
|
|
846
|
+
// duplicating "fp32" here was only correct by coincidence (Messmotor-Sanierung Task 5
|
|
847
|
+
// review finding); it would have silently become a falsehood once a caller constructs
|
|
848
|
+
// the provider with a different dtype (e.g. template mode's "q4").
|
|
849
|
+
dtype: provider.dtype,
|
|
850
|
+
backend: "node-onnx"
|
|
851
|
+
};
|
|
852
|
+
const source = datasetSource;
|
|
853
|
+
consola.start(
|
|
854
|
+
source.kind === "custom" ? `Loading custom dataset (${source.path})...` : `Loading CrowS-Pairs dataset (${source.lang})...`
|
|
855
|
+
);
|
|
856
|
+
const loaded = source.kind === "custom" ? loadCustomDataset(source.path) : { pairs: loadCrowsPairsDataset(source.lang), metadata: void 0 };
|
|
857
|
+
const dataset = loaded.pairs;
|
|
858
|
+
consola.success(`Loaded ${dataset.length} sentence pairs`);
|
|
859
|
+
const datasetSha256 = sha256Hex(JSON.stringify(dataset));
|
|
860
|
+
const selection = selectPairs(dataset, maxPairs, seed);
|
|
861
|
+
if (selection.seed !== null) {
|
|
862
|
+
consola.info(
|
|
863
|
+
`Sampling ${selection.pairs.length} of ${dataset.length} pairs (seed=${selection.seed})`
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
consola.start(`Running CrowS-Pairs bias test on '${model}' (transformers.js)...`);
|
|
867
|
+
const result = await calculateCrowsPairsBias(
|
|
868
|
+
{ dataset: selection.pairs, model, engine: "browser", allowPartial },
|
|
869
|
+
provider
|
|
870
|
+
);
|
|
871
|
+
consola.success(
|
|
872
|
+
`Bias test complete: preference=${result.stereotypicalPreference.toFixed(1)}% of ${result.pairsAnalyzed} pairs, score=${result.score.toFixed(4)}, method=${result.method}, runStatus=${result.runStatus}`
|
|
873
|
+
);
|
|
874
|
+
const scoring = result.scoringProvenance;
|
|
875
|
+
const templateStrategy = scoring?.templateStrategy ?? "none";
|
|
876
|
+
const provenance = {
|
|
877
|
+
...baseProvenance,
|
|
878
|
+
templateStrategy,
|
|
879
|
+
...templateStrategy === "fixed_template_span" ? {
|
|
880
|
+
promptText: scoring?.promptText,
|
|
881
|
+
promptSha256: scoring?.promptSha256,
|
|
882
|
+
contextTokenHash: scoring?.contextTokenHash
|
|
883
|
+
} : {}
|
|
884
|
+
};
|
|
885
|
+
return {
|
|
886
|
+
result,
|
|
887
|
+
sampling: {
|
|
888
|
+
mode: selection.seed === null ? "full" : "seeded_sample",
|
|
889
|
+
datasetSize: dataset.length,
|
|
890
|
+
maxPairs: selection.seed === null ? null : maxPairs,
|
|
891
|
+
seed: selection.seed,
|
|
892
|
+
datasetSha256,
|
|
893
|
+
pairIds: selection.pairs.map(pairIdOf)
|
|
894
|
+
},
|
|
895
|
+
provenance,
|
|
896
|
+
catalog: loaded.metadata
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// src/utils/exit.ts
|
|
901
|
+
import consola2 from "consola";
|
|
484
902
|
function exitWithError(message) {
|
|
485
|
-
|
|
903
|
+
consola2.error(message);
|
|
486
904
|
process.exit(1);
|
|
487
905
|
}
|
|
488
906
|
|
|
489
907
|
// src/commands/bias.ts
|
|
490
908
|
var VALID_LANGS = /* @__PURE__ */ new Set(["en", "de"]);
|
|
491
909
|
var VALID_OUTPUTS = /* @__PURE__ */ new Set(["json", "md", "all"]);
|
|
910
|
+
var MAX_SEED = 4294967295;
|
|
911
|
+
var OLLAMA_DISABLED_MESSAGE = "Ollama liefert keine Prompt-/Satz-Logprobs \u2014 logprobs decken dort nur bereits generierte Tokens ab, nicht die zu bewertenden S\xE4tze. Damit l\xE4sst sich Bias nicht messen. Nutze --engine transformers (Standard) mit einem Hugging-Face-Modell (z.B. onnx-community/Llama-3.2-1B-Instruct oder Xenova/distilgpt2).";
|
|
912
|
+
async function refreshBundle(outDir) {
|
|
913
|
+
const { readFile: readFile6 } = await import("fs/promises");
|
|
914
|
+
const { writeBundleManifest: writeBundleManifest2 } = await import("./writer-OTMAB5JA.js");
|
|
915
|
+
try {
|
|
916
|
+
const report = JSON.parse(await readFile6(resolve2(outDir, "euconform.report.json"), "utf-8"));
|
|
917
|
+
await writeBundleManifest2(outDir, {
|
|
918
|
+
tool: report.tool,
|
|
919
|
+
target: { name: report.target.name, rootPath: report.target.rootPath },
|
|
920
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
921
|
+
});
|
|
922
|
+
} catch {
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
function parseCount(raw, flag, min = 1) {
|
|
926
|
+
if (raw === void 0) return void 0;
|
|
927
|
+
const value = Number(raw);
|
|
928
|
+
if (!Number.isInteger(value) || value < min || value > MAX_SEED) {
|
|
929
|
+
exitWithError(`Invalid ${flag}: ${raw}. Expected an integer between ${min} and ${MAX_SEED}.`);
|
|
930
|
+
}
|
|
931
|
+
return value;
|
|
932
|
+
}
|
|
933
|
+
function resolveDatasetSource(args) {
|
|
934
|
+
if (args.dataset && args.lang) {
|
|
935
|
+
throw new Error("--dataset und --lang k\xF6nnen nicht gleichzeitig verwendet werden.");
|
|
936
|
+
}
|
|
937
|
+
if (args.dataset) return { kind: "custom", path: args.dataset };
|
|
938
|
+
const lang = args.lang ?? "de";
|
|
939
|
+
if (!VALID_LANGS.has(lang)) {
|
|
940
|
+
throw new Error(`Invalid language: ${lang}. Use one of: en, de.`);
|
|
941
|
+
}
|
|
942
|
+
return { kind: "bundled", lang };
|
|
943
|
+
}
|
|
944
|
+
function resolveDatasetSourceOrExit(args) {
|
|
945
|
+
try {
|
|
946
|
+
return resolveDatasetSource(args);
|
|
947
|
+
} catch (error) {
|
|
948
|
+
return exitWithError(error.message);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
function resolveEngine(args) {
|
|
952
|
+
const engine = args.engine ?? "transformers";
|
|
953
|
+
if (engine === "ollama") {
|
|
954
|
+
throw new Error(OLLAMA_DISABLED_MESSAGE);
|
|
955
|
+
}
|
|
956
|
+
if (engine !== "transformers") {
|
|
957
|
+
throw new Error(`Invalid --engine: ${engine}. Use one of: transformers, ollama.`);
|
|
958
|
+
}
|
|
959
|
+
if (args.url !== void 0) {
|
|
960
|
+
throw new Error(
|
|
961
|
+
"--url geh\xF6rt zum Ollama-Modus und kann nicht mit --engine transformers kombiniert werden."
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
return engine;
|
|
965
|
+
}
|
|
966
|
+
function resolveEngineOrExit(args) {
|
|
967
|
+
try {
|
|
968
|
+
return resolveEngine(args);
|
|
969
|
+
} catch (error) {
|
|
970
|
+
return exitWithError(error.message);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
function enforceExactMethod(result, requireExact) {
|
|
974
|
+
const deviatingPairs = result.pairResults.filter((p) => p.method !== result.method);
|
|
975
|
+
if (deviatingPairs.length === 0) return;
|
|
976
|
+
const message = `${deviatingPairs.length} von ${result.pairResults.length} Paaren wurden mit einer von der deklarierten Methode ('${result.method}') abweichenden Methode gemessen.`;
|
|
977
|
+
if (requireExact) exitWithError(`${message} Lauf verworfen (--require-exact).`);
|
|
978
|
+
console.warn(`\u26A0\uFE0F ${message}`);
|
|
979
|
+
}
|
|
980
|
+
async function ensureOutDir(outDir) {
|
|
981
|
+
try {
|
|
982
|
+
await stat(outDir);
|
|
983
|
+
} catch {
|
|
984
|
+
await mkdir(outDir, { recursive: true });
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
function resolveDatasetLabel(datasetSource, catalog) {
|
|
988
|
+
return datasetSource.kind === "bundled" ? `crows_pairs_${datasetSource.lang}` : catalog?.name ?? datasetSource.path;
|
|
989
|
+
}
|
|
990
|
+
async function writeJsonReport(outDir, biasOutput) {
|
|
991
|
+
const jsonPath = resolve2(outDir, "euconform.bias.json");
|
|
992
|
+
await writeFile(jsonPath, JSON.stringify(biasOutput, null, 2));
|
|
993
|
+
consola3.success(`Written: ${jsonPath}`);
|
|
994
|
+
await refreshBundle(outDir);
|
|
995
|
+
}
|
|
996
|
+
function formatWilson(interval) {
|
|
997
|
+
if (!interval) return "n/a";
|
|
998
|
+
return `[${(interval.lower * 100).toFixed(1)}%, ${(interval.upper * 100).toFixed(1)}%]`;
|
|
999
|
+
}
|
|
1000
|
+
function buildMarkdownReport(params) {
|
|
1001
|
+
const { model, datasetSource, datasetLabel, generatedAt, result, sampling, statistics } = params;
|
|
1002
|
+
const failedPairsLines = result.failedPairs.length === 0 ? [] : [
|
|
1003
|
+
"",
|
|
1004
|
+
"## Failed Pairs",
|
|
1005
|
+
"",
|
|
1006
|
+
"| Pair Id | Reason |",
|
|
1007
|
+
"|---------|--------|",
|
|
1008
|
+
...result.failedPairs.map((p) => `| ${p.pairId ?? "(none)"} | ${p.reason} |`)
|
|
1009
|
+
];
|
|
1010
|
+
return [
|
|
1011
|
+
"# EuConform Bias Test Report",
|
|
1012
|
+
"",
|
|
1013
|
+
`**Model:** ${model}`,
|
|
1014
|
+
`**Dataset:** ${datasetSource.kind === "bundled" ? `CrowS-Pairs (${datasetSource.lang.toUpperCase()})` : datasetLabel}`,
|
|
1015
|
+
`**Generated:** ${generatedAt}`,
|
|
1016
|
+
`**Method:** autoregressive CrowS-Pairs adaptation (${result.method})`,
|
|
1017
|
+
"",
|
|
1018
|
+
"## Results",
|
|
1019
|
+
"",
|
|
1020
|
+
"| Metric | Value |",
|
|
1021
|
+
"|--------|-------|",
|
|
1022
|
+
`| Bias Score (mean) | ${result.score.toFixed(4)} |`,
|
|
1023
|
+
`| Run Status | ${result.runStatus} |`,
|
|
1024
|
+
`| Selected / Analyzed / Failed | ${result.selectedCount} / ${result.analyzedCount} / ${result.failedCount} |`,
|
|
1025
|
+
`| Preference Quote (bias > 0) | ${statistics.preferencePercent.toFixed(1)}% (95% Wilson: ${formatWilson(statistics.preferenceWilson95)}) |`,
|
|
1026
|
+
`| Ties (bias = 0) | ${statistics.tiesCount} |`,
|
|
1027
|
+
`| Equal-token-length subset | ${statistics.equalTokenPairsCount} pairs, preference ${statistics.equalTokenPreferencePercent === null ? "n/a" : `${statistics.equalTokenPreferencePercent.toFixed(1)}%`} |`,
|
|
1028
|
+
`| Pairs Analyzed | ${result.pairsAnalyzed} of ${sampling.datasetSize} |`,
|
|
1029
|
+
`| Sampling | ${sampling.seed === null ? "full dataset" : `${sampling.maxPairs} pairs, seed ${sampling.seed}`} |`,
|
|
1030
|
+
"",
|
|
1031
|
+
"## Bias Score Distribution",
|
|
1032
|
+
"",
|
|
1033
|
+
statistics.pairDiffQuartiles ? [
|
|
1034
|
+
"| min | q1 | median | q3 | max |",
|
|
1035
|
+
"|-----|----|--------|----|----|",
|
|
1036
|
+
`| ${statistics.pairDiffQuartiles.min.toFixed(4)} | ${statistics.pairDiffQuartiles.q1.toFixed(4)} | ${statistics.pairDiffQuartiles.median.toFixed(4)} | ${statistics.pairDiffQuartiles.q3.toFixed(4)} | ${statistics.pairDiffQuartiles.max.toFixed(4)} |`
|
|
1037
|
+
].join("\n") : "n/a",
|
|
1038
|
+
...failedPairsLines,
|
|
1039
|
+
"",
|
|
1040
|
+
"---",
|
|
1041
|
+
"",
|
|
1042
|
+
"*Generated by [EuConform CLI](https://github.com/Hiepler/EuConform) using an autoregressive adaptation of the CrowS-Pairs methodology (Nangia et al., 2020)*",
|
|
1043
|
+
""
|
|
1044
|
+
].join("\n");
|
|
1045
|
+
}
|
|
1046
|
+
async function writeMarkdownReport(outDir, content) {
|
|
1047
|
+
const mdPath = resolve2(outDir, "euconform.bias.md");
|
|
1048
|
+
await writeFile(mdPath, content);
|
|
1049
|
+
consola3.success(`Written: ${mdPath}`);
|
|
1050
|
+
}
|
|
492
1051
|
var bias_default = defineCommand({
|
|
493
1052
|
meta: {
|
|
494
1053
|
name: "bias",
|
|
495
|
-
description: "Run CrowS-Pairs bias test on
|
|
1054
|
+
description: "Run CrowS-Pairs bias test on a Hugging Face model via transformers.js"
|
|
496
1055
|
},
|
|
497
1056
|
args: {
|
|
498
1057
|
model: {
|
|
499
1058
|
type: "positional",
|
|
500
|
-
description: "
|
|
1059
|
+
description: "Hugging Face model id (e.g. onnx-community/Llama-3.2-1B-Instruct, Xenova/distilgpt2)",
|
|
501
1060
|
required: true
|
|
502
1061
|
},
|
|
1062
|
+
engine: {
|
|
1063
|
+
type: "string",
|
|
1064
|
+
description: "Measurement engine: transformers (default; the only one that measures real sentence log-probabilities) or ollama (disabled \u2014 see error message)."
|
|
1065
|
+
},
|
|
1066
|
+
"allow-partial": {
|
|
1067
|
+
type: "boolean",
|
|
1068
|
+
description: "Diagnose, nicht als vollst\xE4ndige Messung geeignet: Paare, deren Messung fehlschl\xE4gt, landen in failedPairs statt den Lauf abzubrechen (runStatus wird 'partial')."
|
|
1069
|
+
},
|
|
503
1070
|
lang: {
|
|
504
1071
|
type: "string",
|
|
505
|
-
|
|
506
|
-
|
|
1072
|
+
// No default here: resolveDatasetSource applies "de" itself. A parser default
|
|
1073
|
+
// would make --lang appear "set" on every invocation, tripping the
|
|
1074
|
+
// --dataset/--lang mutual-exclusion check even when the user only passed --dataset.
|
|
1075
|
+
description: "Dataset language: en or de"
|
|
1076
|
+
},
|
|
1077
|
+
dataset: {
|
|
1078
|
+
type: "string",
|
|
1079
|
+
description: "Pfad zu einem eigenen Fragekatalog (JSON). Nicht mit --lang kombinierbar."
|
|
1080
|
+
},
|
|
1081
|
+
"require-exact": {
|
|
1082
|
+
type: "boolean",
|
|
1083
|
+
description: "Abbruch, wenn ein Paar nicht mit exakten Log-Probabilities gemessen wurde."
|
|
507
1084
|
},
|
|
508
1085
|
url: {
|
|
509
1086
|
type: "string",
|
|
510
|
-
description: "Ollama base URL"
|
|
511
|
-
default: "http://localhost:11434"
|
|
1087
|
+
description: "Ollama base URL (Ollama-Modus ist deaktiviert \u2014 siehe --engine)."
|
|
512
1088
|
},
|
|
513
1089
|
output: {
|
|
514
1090
|
type: "string",
|
|
@@ -519,1108 +1095,701 @@ var bias_default = defineCommand({
|
|
|
519
1095
|
type: "string",
|
|
520
1096
|
description: "Output directory",
|
|
521
1097
|
default: "./.euconform"
|
|
1098
|
+
},
|
|
1099
|
+
"max-pairs": {
|
|
1100
|
+
type: "string",
|
|
1101
|
+
description: "Limit the run to N randomly sampled sentence pairs"
|
|
1102
|
+
},
|
|
1103
|
+
seed: {
|
|
1104
|
+
type: "string",
|
|
1105
|
+
description: "Seed for the sample selection, making the run reproducible"
|
|
522
1106
|
}
|
|
523
1107
|
},
|
|
524
1108
|
async run({ args }) {
|
|
525
|
-
|
|
1109
|
+
resolveEngineOrExit({
|
|
1110
|
+
engine: args.engine,
|
|
1111
|
+
url: args.url
|
|
1112
|
+
});
|
|
526
1113
|
const output = args.output;
|
|
527
|
-
const outDir =
|
|
1114
|
+
const outDir = resolve2(args["out-dir"]);
|
|
528
1115
|
const model = args.model;
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
1116
|
+
const allowPartial = Boolean(args["allow-partial"]);
|
|
1117
|
+
const maxPairs = parseCount(args["max-pairs"], "--max-pairs");
|
|
1118
|
+
const seed = parseCount(args.seed, "--seed", 0);
|
|
1119
|
+
const datasetSource = resolveDatasetSourceOrExit({
|
|
1120
|
+
lang: args.lang,
|
|
1121
|
+
dataset: args.dataset
|
|
1122
|
+
});
|
|
533
1123
|
if (!VALID_OUTPUTS.has(output)) {
|
|
534
1124
|
exitWithError(`Invalid output format: ${output}. Use one of: json, md, all.`);
|
|
535
1125
|
}
|
|
536
|
-
|
|
1126
|
+
if (seed !== void 0 && maxPairs === void 0) {
|
|
1127
|
+
consola3.warn("--seed only affects sampling; without --max-pairs the full dataset is used.");
|
|
1128
|
+
}
|
|
1129
|
+
if (allowPartial) {
|
|
1130
|
+
consola3.warn(
|
|
1131
|
+
"--allow-partial: Diagnosemodus, nicht als vollst\xE4ndige Messung geeignet. Fehlgeschlagene Paare werden \xFCbersprungen statt den Lauf abzubrechen."
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
const { result, sampling, provenance, catalog } = await runBiasTest({
|
|
537
1135
|
model,
|
|
538
|
-
|
|
539
|
-
|
|
1136
|
+
datasetSource,
|
|
1137
|
+
maxPairs,
|
|
1138
|
+
seed,
|
|
1139
|
+
allowPartial
|
|
540
1140
|
});
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
1141
|
+
if (result.runStatus === "partial") {
|
|
1142
|
+
consola3.warn(
|
|
1143
|
+
`${result.failedCount} von ${result.selectedCount} Paaren konnten nicht gemessen werden (runStatus: partial, --allow-partial). Dies ist eine Diagnose, keine vollst\xE4ndige Messung.`
|
|
1144
|
+
);
|
|
545
1145
|
}
|
|
1146
|
+
enforceExactMethod(result, Boolean(args["require-exact"]));
|
|
1147
|
+
await ensureOutDir(outDir);
|
|
1148
|
+
const datasetLabel = resolveDatasetLabel(datasetSource, catalog);
|
|
1149
|
+
const rawScore = result.pairResults.reduce((sum, p) => sum + p.biasScore, 0) / result.pairResults.length;
|
|
1150
|
+
const statistics = buildBiasStatistics(result.pairResults);
|
|
546
1151
|
const biasOutput = {
|
|
547
|
-
schemaVersion: "euconform.bias.
|
|
1152
|
+
schemaVersion: "euconform.bias.v2",
|
|
548
1153
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
549
1154
|
model,
|
|
550
|
-
dataset:
|
|
1155
|
+
dataset: datasetLabel,
|
|
1156
|
+
provenance,
|
|
1157
|
+
sampling,
|
|
551
1158
|
result: {
|
|
552
|
-
score:
|
|
1159
|
+
score: rawScore,
|
|
553
1160
|
method: result.method,
|
|
554
|
-
pairsAnalyzed: result.pairsAnalyzed
|
|
555
|
-
|
|
556
|
-
|
|
1161
|
+
pairsAnalyzed: result.pairsAnalyzed
|
|
1162
|
+
},
|
|
1163
|
+
runStatus: result.runStatus,
|
|
1164
|
+
selectedCount: result.selectedCount,
|
|
1165
|
+
analyzedCount: result.analyzedCount,
|
|
1166
|
+
failedCount: result.failedCount,
|
|
1167
|
+
failedPairs: result.failedPairs,
|
|
1168
|
+
statistics,
|
|
1169
|
+
pairResults: result.pairResults,
|
|
1170
|
+
// Only present for custom catalogs — a bundled dataset has no catalog to attribute,
|
|
1171
|
+
// and emitting a fabricated one would misrepresent the run's provenance.
|
|
1172
|
+
...catalog ? { catalog } : {}
|
|
557
1173
|
};
|
|
558
|
-
const severity = formatBiasSeverity(result.score);
|
|
559
1174
|
if (output === "json" || output === "all") {
|
|
560
|
-
|
|
561
|
-
await writeFile(jsonPath, JSON.stringify(biasOutput, null, 2));
|
|
562
|
-
consola2.success(`Written: ${jsonPath}`);
|
|
1175
|
+
await writeJsonReport(outDir, biasOutput);
|
|
563
1176
|
}
|
|
564
1177
|
if (output === "md" || output === "all") {
|
|
565
|
-
const mdContent =
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
"|--------|-------|",
|
|
576
|
-
`| Bias Score | ${result.score.toFixed(4)} |`,
|
|
577
|
-
`| Severity | ${severity} |`,
|
|
578
|
-
`| Method | ${result.method} |`,
|
|
579
|
-
`| Pairs Analyzed | ${result.pairsAnalyzed} |`,
|
|
580
|
-
`| Stereotypical Preference | ${result.stereotypicalPreference.toFixed(1)}% |`,
|
|
581
|
-
"",
|
|
582
|
-
"## Thresholds",
|
|
583
|
-
"",
|
|
584
|
-
"| Range | Classification |",
|
|
585
|
-
"|-------|---------------|",
|
|
586
|
-
"| < 0.1 | Minimal Bias |",
|
|
587
|
-
"| 0.1 - 0.3 | Light Bias |",
|
|
588
|
-
"| > 0.3 | Strong Bias |",
|
|
589
|
-
"",
|
|
590
|
-
"---",
|
|
591
|
-
"",
|
|
592
|
-
"*Generated by [EuConform CLI](https://github.com/Hiepler/EuConform) using CrowS-Pairs methodology (Nangia et al., 2020)*",
|
|
593
|
-
""
|
|
594
|
-
].join("\n");
|
|
595
|
-
const mdPath = resolve(outDir, "euconform.bias.md");
|
|
596
|
-
await writeFile(mdPath, mdContent);
|
|
597
|
-
consola2.success(`Written: ${mdPath}`);
|
|
1178
|
+
const mdContent = buildMarkdownReport({
|
|
1179
|
+
model,
|
|
1180
|
+
datasetSource,
|
|
1181
|
+
datasetLabel,
|
|
1182
|
+
generatedAt: biasOutput.generatedAt,
|
|
1183
|
+
result,
|
|
1184
|
+
sampling,
|
|
1185
|
+
statistics
|
|
1186
|
+
});
|
|
1187
|
+
await writeMarkdownReport(outDir, mdContent);
|
|
598
1188
|
}
|
|
599
1189
|
}
|
|
600
1190
|
});
|
|
601
1191
|
|
|
602
|
-
// src/commands/
|
|
603
|
-
import { stat as
|
|
604
|
-
import { resolve as resolve3 } from "path";
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
var
|
|
608
|
-
var
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
"pages",
|
|
612
|
-
"routes",
|
|
613
|
-
"route",
|
|
614
|
-
"components",
|
|
615
|
-
"ui",
|
|
616
|
-
"views",
|
|
617
|
-
"view",
|
|
618
|
-
"screens",
|
|
619
|
-
"screen",
|
|
620
|
-
"templates",
|
|
621
|
-
"template",
|
|
622
|
-
"public",
|
|
623
|
-
"content",
|
|
624
|
-
"locales",
|
|
625
|
-
"messages",
|
|
626
|
-
"copy",
|
|
627
|
-
"emails"
|
|
628
|
-
]);
|
|
629
|
-
var USER_FACING_BASENAME_PATTERN = /(?:page|layout|route|template|loading|error|view|screen|dialog|modal|form|widget|banner|notice)\.[^.]+$/i;
|
|
630
|
-
function normalizedProvenance(provenance) {
|
|
631
|
-
return provenance ?? "runtime";
|
|
632
|
-
}
|
|
633
|
-
function isInternalAnalysisPath(relativePath) {
|
|
634
|
-
const normalized = relativePath.replace(/\\/g, "/").toLowerCase();
|
|
635
|
-
return normalized.includes("/scanner/") || normalized.includes("/detectors/");
|
|
636
|
-
}
|
|
637
|
-
function isUserFacingPath(relativePath) {
|
|
638
|
-
const normalized = relativePath.replace(/\\/g, "/").toLowerCase();
|
|
639
|
-
const segments = normalized.split("/").filter(Boolean);
|
|
640
|
-
const basename4 = segments[segments.length - 1] ?? "";
|
|
641
|
-
return segments.some((segment) => USER_FACING_SEGMENTS.has(segment)) || USER_FACING_BASENAME_PATTERN.test(basename4);
|
|
642
|
-
}
|
|
643
|
-
function isImplementationEvidence(category, evidence) {
|
|
644
|
-
const provenance = normalizedProvenance(evidence.provenance);
|
|
645
|
-
if (!IMPLEMENTATION_PROVENANCE.has(provenance)) return false;
|
|
646
|
-
if (isInternalAnalysisPath(evidence.file)) return false;
|
|
647
|
-
if (category === "compliance-disclosure") {
|
|
648
|
-
return isUserFacingPath(evidence.file);
|
|
649
|
-
}
|
|
650
|
-
return true;
|
|
651
|
-
}
|
|
652
|
-
function isSupportingEvidence(category, evidence) {
|
|
653
|
-
const provenance = normalizedProvenance(evidence.provenance);
|
|
654
|
-
if (!SUPPORTING_PROVENANCE.has(provenance)) return false;
|
|
655
|
-
if (category === "compliance-disclosure") return true;
|
|
656
|
-
if (category.startsWith("compliance-")) return true;
|
|
657
|
-
return false;
|
|
658
|
-
}
|
|
659
|
-
function withFilteredEvidence(signal, matcher) {
|
|
660
|
-
const evidence = signal.evidence.filter(matcher);
|
|
661
|
-
if (evidence.length === 0) return null;
|
|
662
|
-
return { ...signal, evidence };
|
|
663
|
-
}
|
|
664
|
-
function evaluateSignalsForCategories(signals, categories) {
|
|
665
|
-
const relevant = signals.filter((signal) => categories.includes(signal.category));
|
|
666
|
-
return {
|
|
667
|
-
implementationSignals: relevant.map(
|
|
668
|
-
(signal) => withFilteredEvidence(
|
|
669
|
-
signal,
|
|
670
|
-
(evidence) => isImplementationEvidence(signal.category, evidence)
|
|
671
|
-
)
|
|
672
|
-
).filter((signal) => signal !== null),
|
|
673
|
-
supportingSignals: relevant.map(
|
|
674
|
-
(signal) => withFilteredEvidence(signal, (evidence) => isSupportingEvidence(signal.category, evidence))
|
|
675
|
-
).filter((signal) => signal !== null)
|
|
676
|
-
};
|
|
677
|
-
}
|
|
678
|
-
function hasImplementationSignals(signals, categories) {
|
|
679
|
-
return evaluateSignalsForCategories(signals, categories).implementationSignals.length > 0;
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
// ../core/src/evidence/markdown.ts
|
|
683
|
-
var COMPLIANCE_LABELS = {
|
|
684
|
-
disclosure: "AI Disclosure",
|
|
685
|
-
biasTesting: "Bias Testing",
|
|
686
|
-
reportingExports: "Reporting & Exports",
|
|
687
|
-
loggingMonitoring: "Logging & Monitoring",
|
|
688
|
-
humanOversight: "Human Oversight",
|
|
689
|
-
dataGovernance: "Data Governance",
|
|
690
|
-
incidentReporting: "Incident Reporting"
|
|
691
|
-
};
|
|
692
|
-
var STATUS_LABELS = {
|
|
693
|
-
present: "Present",
|
|
694
|
-
partial: "Partial",
|
|
695
|
-
absent: "Absent",
|
|
696
|
-
unknown: "Unknown"
|
|
697
|
-
};
|
|
698
|
-
function bomKindLabel(kind) {
|
|
699
|
-
const labels = {
|
|
700
|
-
framework: "Framework",
|
|
701
|
-
runtime: "Runtime",
|
|
702
|
-
"inference-provider": "Inference Provider",
|
|
703
|
-
"ai-framework": "AI Framework",
|
|
704
|
-
model: "Model",
|
|
705
|
-
"vector-store": "Vector Store",
|
|
706
|
-
embedding: "Embedding",
|
|
707
|
-
dataset: "Dataset",
|
|
708
|
-
tool: "Tool"
|
|
709
|
-
};
|
|
710
|
-
return labels[kind];
|
|
711
|
-
}
|
|
712
|
-
function formatDate(iso) {
|
|
1192
|
+
// src/commands/compare.ts
|
|
1193
|
+
import { mkdir as mkdir2, readFile, stat as stat2, writeFile as writeFile2 } from "fs/promises";
|
|
1194
|
+
import { join, resolve as resolve3 } from "path";
|
|
1195
|
+
import { defineCommand as defineCommand2 } from "citty";
|
|
1196
|
+
import consola4 from "consola";
|
|
1197
|
+
var VALID_OUTPUTS2 = /* @__PURE__ */ new Set(["md", "json", "all"]);
|
|
1198
|
+
var BIAS_ARTIFACT_NAME = "euconform.bias.json";
|
|
1199
|
+
async function resolveArtifactPath(rawPath) {
|
|
1200
|
+
const absolute = resolve3(rawPath);
|
|
713
1201
|
try {
|
|
714
|
-
const
|
|
715
|
-
|
|
1202
|
+
const info = await stat2(absolute);
|
|
1203
|
+
if (info.isDirectory()) return join(absolute, BIAS_ARTIFACT_NAME);
|
|
1204
|
+
return absolute;
|
|
716
1205
|
} catch {
|
|
717
|
-
return
|
|
1206
|
+
return exitWithError(`Pfad nicht gefunden: ${rawPath}`);
|
|
718
1207
|
}
|
|
719
1208
|
}
|
|
720
|
-
function
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
`> Generated by EuConform v${report.tool.version} on ${date}.`,
|
|
727
|
-
`> Scan scope: \`${scanScope}\`.`
|
|
728
|
-
].join("\n");
|
|
729
|
-
}
|
|
730
|
-
function buildProjectOverview(report) {
|
|
731
|
-
const lines = [
|
|
732
|
-
"## Project Overview",
|
|
733
|
-
"",
|
|
734
|
-
`- **Name:** ${report.target.name}`,
|
|
735
|
-
`- **Type:** ${report.target.repoType}`
|
|
736
|
-
];
|
|
737
|
-
if (report.target.detectedStack.length > 0) {
|
|
738
|
-
lines.push(`- **Stack:** ${report.target.detectedStack.join(", ")}`);
|
|
739
|
-
} else {
|
|
740
|
-
lines.push("- **Stack:** Not detected");
|
|
741
|
-
}
|
|
742
|
-
return lines.join("\n");
|
|
743
|
-
}
|
|
744
|
-
function buildAiComponents(aibom) {
|
|
745
|
-
const lines = ["## AI Components Detected", ""];
|
|
746
|
-
if (aibom.components.length === 0) {
|
|
747
|
-
lines.push("No AI components detected.");
|
|
748
|
-
return lines.join("\n");
|
|
749
|
-
}
|
|
750
|
-
lines.push("| Component | Kind | Source |");
|
|
751
|
-
lines.push("|-----------|------|--------|");
|
|
752
|
-
for (const comp of aibom.components) {
|
|
753
|
-
lines.push(`| ${comp.name} | ${bomKindLabel(comp.kind)} | ${comp.source} |`);
|
|
754
|
-
}
|
|
755
|
-
return lines.join("\n");
|
|
756
|
-
}
|
|
757
|
-
function buildComplianceSignals(report) {
|
|
758
|
-
const lines = ["## Compliance Signals", ""];
|
|
759
|
-
lines.push("| Area | Status | Confidence |");
|
|
760
|
-
lines.push("|------|--------|------------|");
|
|
761
|
-
const areas = Object.keys(COMPLIANCE_LABELS);
|
|
762
|
-
for (const area of areas) {
|
|
763
|
-
const group = report.complianceSignals[area];
|
|
764
|
-
lines.push(
|
|
765
|
-
`| ${COMPLIANCE_LABELS[area]} | ${STATUS_LABELS[group.status]} | ${group.confidence} |`
|
|
766
|
-
);
|
|
767
|
-
}
|
|
768
|
-
return lines.join("\n");
|
|
769
|
-
}
|
|
770
|
-
function buildAssessmentHints(report) {
|
|
771
|
-
const { riskIndicators, gpaiIndicators, possibleModes } = report.assessmentHints;
|
|
772
|
-
const lines = ["## Assessment Hints", ""];
|
|
773
|
-
if (gpaiIndicators.length === 0 && riskIndicators.length === 0 && possibleModes.length === 0) {
|
|
774
|
-
lines.push("No assessment hints generated (no AI detected).");
|
|
775
|
-
return lines.join("\n");
|
|
776
|
-
}
|
|
777
|
-
if (possibleModes.length > 0) {
|
|
778
|
-
lines.push(`**Possible classification modes:** ${possibleModes.join(", ")}`);
|
|
779
|
-
lines.push("");
|
|
780
|
-
}
|
|
781
|
-
if (gpaiIndicators.length > 0) {
|
|
782
|
-
lines.push("### GPAI Indicators");
|
|
783
|
-
lines.push("");
|
|
784
|
-
for (const hint of gpaiIndicators) {
|
|
785
|
-
const ref = hint.articleRef ? ` (${hint.articleRef})` : "";
|
|
786
|
-
lines.push(`- ${hint.hint}${ref}`);
|
|
787
|
-
}
|
|
788
|
-
lines.push("");
|
|
789
|
-
}
|
|
790
|
-
if (riskIndicators.length > 0) {
|
|
791
|
-
lines.push("### Risk Indicators");
|
|
792
|
-
lines.push("");
|
|
793
|
-
for (const hint of riskIndicators) {
|
|
794
|
-
const ref = hint.articleRef ? ` (${hint.articleRef})` : "";
|
|
795
|
-
lines.push(`- ${hint.hint}${ref}`);
|
|
796
|
-
}
|
|
797
|
-
lines.push("");
|
|
798
|
-
}
|
|
799
|
-
return lines.join("\n");
|
|
800
|
-
}
|
|
801
|
-
function buildOpenQuestions(report) {
|
|
802
|
-
const questions = report.assessmentHints.openQuestions;
|
|
803
|
-
const lines = ["## Open Questions", ""];
|
|
804
|
-
if (questions.length === 0) {
|
|
805
|
-
lines.push("No open questions.");
|
|
806
|
-
return lines.join("\n");
|
|
807
|
-
}
|
|
808
|
-
questions.forEach((q, i) => {
|
|
809
|
-
lines.push(`${i + 1}. ${q}`);
|
|
810
|
-
});
|
|
811
|
-
return lines.join("\n");
|
|
812
|
-
}
|
|
813
|
-
function buildRecommendedActions(report) {
|
|
814
|
-
const lines = ["## Recommended Actions", ""];
|
|
815
|
-
if (report.recommendationSummary.length === 0) {
|
|
816
|
-
lines.push("No recommended actions.");
|
|
817
|
-
return lines.join("\n");
|
|
818
|
-
}
|
|
819
|
-
for (const rec of report.recommendationSummary) {
|
|
820
|
-
lines.push(`- ${rec}`);
|
|
821
|
-
}
|
|
822
|
-
return lines.join("\n");
|
|
823
|
-
}
|
|
824
|
-
function buildFooter() {
|
|
825
|
-
return ["---", "*Full details: `euconform.report.json` -- AI BOM: `euconform.aibom.json`*"].join(
|
|
826
|
-
"\n"
|
|
827
|
-
);
|
|
828
|
-
}
|
|
829
|
-
function generateSummaryMarkdown(report, aibom, scanScope) {
|
|
830
|
-
const sections = [
|
|
831
|
-
buildHeader(report, scanScope),
|
|
832
|
-
buildProjectOverview(report),
|
|
833
|
-
buildAiComponents(aibom),
|
|
834
|
-
buildComplianceSignals(report),
|
|
835
|
-
buildAssessmentHints(report),
|
|
836
|
-
buildOpenQuestions(report),
|
|
837
|
-
buildRecommendedActions(report),
|
|
838
|
-
buildFooter()
|
|
839
|
-
];
|
|
840
|
-
return `${sections.join("\n\n")}
|
|
841
|
-
`;
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
// ../core/src/evidence/output.ts
|
|
845
|
-
var COMPLIANCE_AREA_MAP = {
|
|
846
|
-
disclosure: ["compliance-disclosure"],
|
|
847
|
-
biasTesting: ["compliance-bias"],
|
|
848
|
-
reportingExports: ["compliance-reporting"],
|
|
849
|
-
loggingMonitoring: ["compliance-logging"],
|
|
850
|
-
humanOversight: ["compliance-oversight"],
|
|
851
|
-
dataGovernance: ["compliance-data"],
|
|
852
|
-
incidentReporting: ["compliance-incident"]
|
|
853
|
-
};
|
|
854
|
-
var COMPLIANCE_AREA_LABELS = {
|
|
855
|
-
disclosure: "AI disclosure",
|
|
856
|
-
biasTesting: "bias testing",
|
|
857
|
-
reportingExports: "reporting and exports",
|
|
858
|
-
loggingMonitoring: "logging and monitoring",
|
|
859
|
-
humanOversight: "human oversight",
|
|
860
|
-
dataGovernance: "data governance",
|
|
861
|
-
incidentReporting: "incident reporting"
|
|
862
|
-
};
|
|
863
|
-
var AI_CATEGORIES = /* @__PURE__ */ new Set([
|
|
864
|
-
"ai-provider",
|
|
865
|
-
"ai-framework",
|
|
866
|
-
"ai-model",
|
|
867
|
-
"local-inference"
|
|
868
|
-
]);
|
|
869
|
-
var BOM_CATEGORIES = /* @__PURE__ */ new Set([
|
|
870
|
-
"ai-provider",
|
|
871
|
-
"ai-framework",
|
|
872
|
-
"ai-model",
|
|
873
|
-
"local-inference",
|
|
874
|
-
"rag",
|
|
875
|
-
"training",
|
|
876
|
-
"framework",
|
|
877
|
-
"runtime"
|
|
878
|
-
]);
|
|
879
|
-
var CATEGORY_TO_BOM_KIND = {
|
|
880
|
-
"ai-provider": "inference-provider",
|
|
881
|
-
"ai-framework": "ai-framework",
|
|
882
|
-
"ai-model": "model",
|
|
883
|
-
"local-inference": "inference-provider",
|
|
884
|
-
framework: "framework",
|
|
885
|
-
runtime: "runtime",
|
|
886
|
-
rag: "vector-store",
|
|
887
|
-
training: "dataset"
|
|
888
|
-
};
|
|
889
|
-
var SOURCE_PRIORITY = {
|
|
890
|
-
"package.json": 5,
|
|
891
|
-
config: 4,
|
|
892
|
-
code: 3,
|
|
893
|
-
"lock-file": 2,
|
|
894
|
-
docs: 1
|
|
895
|
-
};
|
|
896
|
-
var EVIDENCE_PROVENANCE_PRIORITY = {
|
|
897
|
-
runtime: 5,
|
|
898
|
-
config: 4,
|
|
899
|
-
tooling: 3,
|
|
900
|
-
"root-docs": 2,
|
|
901
|
-
docs: 1,
|
|
902
|
-
test: 0,
|
|
903
|
-
fixture: 0,
|
|
904
|
-
example: 0,
|
|
905
|
-
generated: 0
|
|
906
|
-
};
|
|
907
|
-
function hasAI(signals) {
|
|
908
|
-
return signals.some((signal) => AI_CATEGORIES.has(signal.category));
|
|
909
|
-
}
|
|
910
|
-
function highestConfidence(signals) {
|
|
911
|
-
if (signals.some((signal) => signal.confidence === "high")) return "high";
|
|
912
|
-
if (signals.some((signal) => signal.confidence === "medium")) return "medium";
|
|
913
|
-
return "low";
|
|
914
|
-
}
|
|
915
|
-
function collectEvidence(signals) {
|
|
916
|
-
return normalizeEvidence(signals.flatMap((signal) => signal.evidence));
|
|
917
|
-
}
|
|
918
|
-
function uniquePush(items, value) {
|
|
919
|
-
if (!items.includes(value)) items.push(value);
|
|
920
|
-
}
|
|
921
|
-
function toKebabCase(value) {
|
|
922
|
-
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
923
|
-
}
|
|
924
|
-
function makeBomComponentId(kind, name) {
|
|
925
|
-
const slug = toKebabCase(name);
|
|
926
|
-
return `${kind}:${slug || "component"}`;
|
|
927
|
-
}
|
|
928
|
-
function compareEvidence(left, right) {
|
|
929
|
-
const leftPriority = left.provenance ? EVIDENCE_PROVENANCE_PRIORITY[left.provenance] : 0;
|
|
930
|
-
const rightPriority = right.provenance ? EVIDENCE_PROVENANCE_PRIORITY[right.provenance] : 0;
|
|
931
|
-
if (leftPriority !== rightPriority) {
|
|
932
|
-
return rightPriority - leftPriority;
|
|
933
|
-
}
|
|
934
|
-
if (left.file !== right.file) {
|
|
935
|
-
return left.file.localeCompare(right.file);
|
|
936
|
-
}
|
|
937
|
-
return (left.line ?? 0) - (right.line ?? 0);
|
|
938
|
-
}
|
|
939
|
-
function normalizeEvidence(evidence, maxItems = 12) {
|
|
940
|
-
const unique = /* @__PURE__ */ new Map();
|
|
941
|
-
for (const item of evidence) {
|
|
942
|
-
const key = `${item.file}:${item.line ?? 0}:${item.snippet}`;
|
|
943
|
-
if (!unique.has(key)) {
|
|
944
|
-
unique.set(key, item);
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
return [...unique.values()].sort(compareEvidence).slice(0, maxItems);
|
|
948
|
-
}
|
|
949
|
-
var SIGNAL_NAME_MAP = [
|
|
950
|
-
["react", "React"],
|
|
951
|
-
["express", "Express"],
|
|
952
|
-
["fastify", "Fastify"],
|
|
953
|
-
["nestjs", "NestJS"],
|
|
954
|
-
["nodejs", "Node.js"],
|
|
955
|
-
["langchain", "LangChain"],
|
|
956
|
-
["llamaindex", "LlamaIndex"],
|
|
957
|
-
["transformers", "Transformers.js"],
|
|
958
|
-
["anthropic", "Anthropic"],
|
|
959
|
-
["azure", "Azure OpenAI"],
|
|
960
|
-
["ollama", "Ollama"],
|
|
961
|
-
["onnx", "ONNX Runtime"],
|
|
962
|
-
["chroma", "ChromaDB"],
|
|
963
|
-
["qdrant", "Qdrant"],
|
|
964
|
-
["pinecone", "Pinecone"],
|
|
965
|
-
["weaviate", "Weaviate"],
|
|
966
|
-
["pgvector", "pgvector"]
|
|
967
|
-
];
|
|
968
|
-
function canonicalizeSignalName(signal) {
|
|
969
|
-
const id = signal.id.toLowerCase();
|
|
970
|
-
const lowerName = signal.name.toLowerCase();
|
|
971
|
-
if (id.includes("next") || lowerName === "nextjs") return "Next.js";
|
|
972
|
-
if (id.includes("openai")) return signal.category === "ai-provider" ? "OpenAI" : "OpenAI SDK";
|
|
973
|
-
if (id.includes("google") || id.includes("gemini")) return "Google AI";
|
|
974
|
-
if (id.includes("llamacpp") || lowerName.includes("llama.cpp")) return "llama.cpp";
|
|
975
|
-
if (id.includes("turborepo") || lowerName.includes("turborepo")) return "Monorepo (Turborepo)";
|
|
976
|
-
for (const [key, canonical] of SIGNAL_NAME_MAP) {
|
|
977
|
-
if (id.includes(key)) return canonical;
|
|
978
|
-
}
|
|
979
|
-
return signal.name;
|
|
980
|
-
}
|
|
981
|
-
var LOCAL_INFERENCE_MAP = [
|
|
982
|
-
["ollama", "local-ollama"],
|
|
983
|
-
["transformers", "browser-transformers"],
|
|
984
|
-
["onnx", "local-onnx"],
|
|
985
|
-
["llama", "local-llama"]
|
|
986
|
-
];
|
|
987
|
-
var CLOUD_PROVIDER_MAP = [
|
|
988
|
-
["openai", "cloud-openai"],
|
|
989
|
-
["anthropic", "cloud-anthropic"],
|
|
990
|
-
["google", "cloud-google"],
|
|
991
|
-
["gemini", "cloud-google"],
|
|
992
|
-
["azure", "cloud-azure"]
|
|
993
|
-
];
|
|
994
|
-
function lookupInferenceMode(id, entries, prefix, name) {
|
|
995
|
-
for (const [key, mode] of entries) {
|
|
996
|
-
if (id.includes(key)) return mode;
|
|
997
|
-
}
|
|
998
|
-
const slug = toKebabCase(name);
|
|
999
|
-
return slug ? `${prefix}-${slug}` : null;
|
|
1000
|
-
}
|
|
1001
|
-
function canonicalizeInferenceMode(signal) {
|
|
1002
|
-
const id = signal.id.toLowerCase();
|
|
1003
|
-
const name = canonicalizeSignalName(signal);
|
|
1004
|
-
if (signal.category === "local-inference") {
|
|
1005
|
-
return lookupInferenceMode(id, LOCAL_INFERENCE_MAP, "local", name);
|
|
1006
|
-
}
|
|
1007
|
-
if (signal.category === "ai-provider") {
|
|
1008
|
-
return lookupInferenceMode(id, CLOUD_PROVIDER_MAP, "cloud", name);
|
|
1009
|
-
}
|
|
1010
|
-
return null;
|
|
1011
|
-
}
|
|
1012
|
-
function deriveSource(evidence) {
|
|
1013
|
-
let strongest = "code";
|
|
1014
|
-
for (const item of evidence) {
|
|
1015
|
-
let source = "code";
|
|
1016
|
-
if (item.file.endsWith("package.json")) source = "package.json";
|
|
1017
|
-
else if (item.file.endsWith("pnpm-lock.yaml") || item.file.endsWith("package-lock.json") || item.file.endsWith("yarn.lock")) {
|
|
1018
|
-
source = "lock-file";
|
|
1019
|
-
} else if (item.provenance === "config") {
|
|
1020
|
-
source = "config";
|
|
1021
|
-
} else if (item.provenance === "root-docs" || item.provenance === "docs") {
|
|
1022
|
-
source = "docs";
|
|
1023
|
-
}
|
|
1024
|
-
if (SOURCE_PRIORITY[source] > SOURCE_PRIORITY[strongest]) {
|
|
1025
|
-
strongest = source;
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
return strongest;
|
|
1029
|
-
}
|
|
1030
|
-
function buildAiFootprint(signals) {
|
|
1031
|
-
const usesAI = hasAI(signals);
|
|
1032
|
-
const inferenceModes = [];
|
|
1033
|
-
const providerHints = [];
|
|
1034
|
-
const ragHints = [];
|
|
1035
|
-
for (const signal of signals) {
|
|
1036
|
-
const mode = canonicalizeInferenceMode(signal);
|
|
1037
|
-
if (mode) uniquePush(inferenceModes, mode);
|
|
1038
|
-
if (signal.category === "ai-provider") {
|
|
1039
|
-
uniquePush(providerHints, toKebabCase(canonicalizeSignalName(signal)));
|
|
1040
|
-
}
|
|
1041
|
-
if (signal.category === "rag" && signal.confidence !== "low") {
|
|
1042
|
-
uniquePush(ragHints, canonicalizeSignalName(signal));
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
return { usesAI, inferenceModes, providerHints, ragHints };
|
|
1046
|
-
}
|
|
1047
|
-
function buildComplianceSignalGroup(implementationSignals, aiDetected) {
|
|
1048
|
-
if (implementationSignals.length === 0) {
|
|
1049
|
-
return {
|
|
1050
|
-
status: aiDetected ? "absent" : "unknown",
|
|
1051
|
-
confidence: "low",
|
|
1052
|
-
evidence: []
|
|
1053
|
-
};
|
|
1209
|
+
async function loadBiasReport(path) {
|
|
1210
|
+
let parsed;
|
|
1211
|
+
try {
|
|
1212
|
+
parsed = JSON.parse(await readFile(path, "utf-8"));
|
|
1213
|
+
} catch (error) {
|
|
1214
|
+
return exitWithError(`${path} konnte nicht gelesen werden: ${error.message}`);
|
|
1054
1215
|
}
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
};
|
|
1061
|
-
}
|
|
1062
|
-
function evaluateComplianceAreas(signals, aiDetected) {
|
|
1063
|
-
const result = {};
|
|
1064
|
-
for (const key of Object.keys(COMPLIANCE_AREA_MAP)) {
|
|
1065
|
-
const { implementationSignals, supportingSignals } = evaluateSignalsForCategories(
|
|
1066
|
-
signals,
|
|
1067
|
-
COMPLIANCE_AREA_MAP[key]
|
|
1216
|
+
try {
|
|
1217
|
+
return validateBiasReportV2(parsed);
|
|
1218
|
+
} catch (error) {
|
|
1219
|
+
return exitWithError(
|
|
1220
|
+
`${path} ist kein g\xFCltiges euconform.bias.v2 Artefakt: ${error.message}`
|
|
1068
1221
|
);
|
|
1069
|
-
result[key] = {
|
|
1070
|
-
group: buildComplianceSignalGroup(implementationSignals, aiDetected),
|
|
1071
|
-
implementationSignals,
|
|
1072
|
-
supportingSignals
|
|
1073
|
-
};
|
|
1074
1222
|
}
|
|
1075
|
-
return result;
|
|
1076
1223
|
}
|
|
1077
|
-
function
|
|
1078
|
-
|
|
1079
|
-
const
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
if (!aiDetected) {
|
|
1083
|
-
return {
|
|
1084
|
-
possibleModes: [],
|
|
1085
|
-
riskIndicators: [],
|
|
1086
|
-
gpaiIndicators: [],
|
|
1087
|
-
openQuestions: openQuestions.map((question) => question.question)
|
|
1088
|
-
};
|
|
1224
|
+
function parseTop(raw) {
|
|
1225
|
+
if (raw === void 0) return 10;
|
|
1226
|
+
const value = Number(raw);
|
|
1227
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
1228
|
+
return exitWithError(`Ung\xFCltiges --top: ${raw}. Erwartet eine ganze Zahl >= 1.`);
|
|
1089
1229
|
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
)
|
|
1098
|
-
);
|
|
1099
|
-
gpaiIndicators.push({
|
|
1100
|
-
hint: `GPAI model usage detected \u2014 cloud providers found: ${providers.join(", ")}. Provider obligations depend on your role and downstream distribution.`,
|
|
1101
|
-
articleRef: "Art. 53",
|
|
1102
|
-
confidence: "medium"
|
|
1103
|
-
});
|
|
1104
|
-
}
|
|
1105
|
-
if (hasLocalInference) {
|
|
1106
|
-
gpaiIndicators.push({
|
|
1107
|
-
hint: "Local deployment detected \u2014 provider-side GPAI obligations are less likely unless you distribute or provide downstream access to the model.",
|
|
1108
|
-
articleRef: "Art. 53",
|
|
1109
|
-
confidence: "low"
|
|
1110
|
-
});
|
|
1111
|
-
}
|
|
1112
|
-
if (complianceAreas.biasTesting.group.status !== "absent") {
|
|
1113
|
-
riskIndicators.push({
|
|
1114
|
-
hint: "Bias evaluation infrastructure detected \u2014 this can support Art. 10 evidence if it is part of your real evaluation workflow.",
|
|
1115
|
-
articleRef: "Art. 10",
|
|
1116
|
-
confidence: complianceAreas.biasTesting.group.confidence
|
|
1117
|
-
});
|
|
1118
|
-
}
|
|
1119
|
-
if (signals.some((signal) => signal.category === "training")) {
|
|
1120
|
-
riskIndicators.push({
|
|
1121
|
-
hint: "Training or fine-tuning signals detected \u2014 data governance and provider obligations may apply depending on your role.",
|
|
1122
|
-
articleRef: "Art. 10",
|
|
1123
|
-
confidence: "high"
|
|
1124
|
-
});
|
|
1125
|
-
possibleModes.push("gpai-provider");
|
|
1126
|
-
}
|
|
1127
|
-
for (const [key, evaluation] of Object.entries(complianceAreas)) {
|
|
1128
|
-
if (evaluation.implementationSignals.length > 0 || evaluation.supportingSignals.length === 0) {
|
|
1129
|
-
continue;
|
|
1130
|
-
}
|
|
1131
|
-
riskIndicators.push({
|
|
1132
|
-
hint: `Documentation references ${COMPLIANCE_AREA_LABELS[key]}, but no production implementation evidence was detected.`,
|
|
1133
|
-
confidence: "low"
|
|
1134
|
-
});
|
|
1135
|
-
}
|
|
1136
|
-
if (!possibleModes.includes("annex-iii-unclear")) {
|
|
1137
|
-
possibleModes.push("annex-iii-unclear");
|
|
1230
|
+
return value;
|
|
1231
|
+
}
|
|
1232
|
+
function parseAlpha(raw) {
|
|
1233
|
+
if (raw === void 0) return 0.05;
|
|
1234
|
+
const value = Number(raw);
|
|
1235
|
+
if (!Number.isFinite(value) || value <= 0 || value >= 1) {
|
|
1236
|
+
return exitWithError(`Ung\xFCltiges --alpha: ${raw}. Erwartet eine Zahl zwischen 0 und 1.`);
|
|
1138
1237
|
}
|
|
1139
|
-
return
|
|
1140
|
-
possibleModes,
|
|
1141
|
-
riskIndicators,
|
|
1142
|
-
gpaiIndicators,
|
|
1143
|
-
openQuestions: openQuestions.map((question) => question.question)
|
|
1144
|
-
};
|
|
1238
|
+
return value;
|
|
1145
1239
|
}
|
|
1146
|
-
var
|
|
1147
|
-
{
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
description: "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.",
|
|
1151
|
-
priority: "critical",
|
|
1152
|
-
area: "disclosure"
|
|
1153
|
-
},
|
|
1154
|
-
{
|
|
1155
|
-
id: "gap-logging",
|
|
1156
|
-
title: "No logging or monitoring infrastructure detected",
|
|
1157
|
-
description: "AI system outputs and decisions should be logged for traceability and audit purposes. No production logging infrastructure was detected.",
|
|
1158
|
-
priority: "high",
|
|
1159
|
-
area: "loggingMonitoring"
|
|
1160
|
-
},
|
|
1161
|
-
{
|
|
1162
|
-
id: "gap-oversight",
|
|
1163
|
-
title: "No human oversight mechanism detected",
|
|
1164
|
-
description: "High-risk AI systems require human oversight capabilities. No production review or override mechanisms were found.",
|
|
1165
|
-
priority: "high",
|
|
1166
|
-
area: "humanOversight"
|
|
1167
|
-
},
|
|
1168
|
-
{
|
|
1169
|
-
id: "gap-bias",
|
|
1170
|
-
title: "No bias testing infrastructure detected",
|
|
1171
|
-
description: "AI systems should be evaluated for bias and discrimination. No production-grade bias evaluation tooling was found.",
|
|
1172
|
-
priority: "medium",
|
|
1173
|
-
area: "biasTesting"
|
|
1240
|
+
var compare_default = defineCommand2({
|
|
1241
|
+
meta: {
|
|
1242
|
+
name: "compare",
|
|
1243
|
+
description: "Compare two euconform.bias.v2 artifacts (baseline vs candidate): score/preference deltas, per-bias-type shifts, McNemar significance"
|
|
1174
1244
|
},
|
|
1175
|
-
{
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1245
|
+
args: {
|
|
1246
|
+
baseline: {
|
|
1247
|
+
type: "positional",
|
|
1248
|
+
description: "Baseline artifact (euconform.bias.json) or the directory containing it",
|
|
1249
|
+
required: true
|
|
1250
|
+
},
|
|
1251
|
+
candidate: {
|
|
1252
|
+
type: "positional",
|
|
1253
|
+
description: "Candidate artifact (euconform.bias.json) or the directory containing it",
|
|
1254
|
+
required: true
|
|
1255
|
+
},
|
|
1256
|
+
output: {
|
|
1257
|
+
type: "string",
|
|
1258
|
+
description: "Output format: md, json, or all (all requires --out-dir)",
|
|
1259
|
+
default: "md"
|
|
1260
|
+
},
|
|
1261
|
+
"out-dir": {
|
|
1262
|
+
type: "string",
|
|
1263
|
+
description: "Write euconform.bias-comparison.{md,json} into this directory instead of stdout"
|
|
1264
|
+
},
|
|
1265
|
+
top: {
|
|
1266
|
+
type: "string",
|
|
1267
|
+
description: "Number of per-pair changes to list (default 10)"
|
|
1268
|
+
},
|
|
1269
|
+
alpha: {
|
|
1270
|
+
type: "string",
|
|
1271
|
+
description: "Significance level for the McNemar test (default 0.05)"
|
|
1272
|
+
},
|
|
1273
|
+
"allow-incomparable": {
|
|
1274
|
+
type: "boolean",
|
|
1275
|
+
description: "Report deltas even when catalog/method/template differ. The result is then explicitly marked comparable: false."
|
|
1276
|
+
}
|
|
1181
1277
|
},
|
|
1182
|
-
{
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
priority: "medium",
|
|
1187
|
-
area: "incidentReporting"
|
|
1188
|
-
}
|
|
1189
|
-
];
|
|
1190
|
-
function buildGaps(complianceAreas, aiDetected) {
|
|
1191
|
-
if (!aiDetected) return [];
|
|
1192
|
-
const gaps = [];
|
|
1193
|
-
for (const rule of GAP_RULES) {
|
|
1194
|
-
const evaluation = complianceAreas[rule.area];
|
|
1195
|
-
const confidence = highestConfidence(evaluation.implementationSignals);
|
|
1196
|
-
if (evaluation.implementationSignals.length === 0) {
|
|
1197
|
-
gaps.push({
|
|
1198
|
-
id: rule.id,
|
|
1199
|
-
title: rule.title,
|
|
1200
|
-
description: rule.description,
|
|
1201
|
-
priority: rule.priority,
|
|
1202
|
-
status: "missing",
|
|
1203
|
-
basis: "scanner-rule",
|
|
1204
|
-
evidence: []
|
|
1205
|
-
});
|
|
1206
|
-
continue;
|
|
1278
|
+
async run({ args }) {
|
|
1279
|
+
const output = args.output;
|
|
1280
|
+
if (!VALID_OUTPUTS2.has(output)) {
|
|
1281
|
+
exitWithError(`Invalid output format: ${output}. Use one of: md, json, all.`);
|
|
1207
1282
|
}
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
priority: rule.priority === "critical" ? "high" : rule.priority === "high" ? "medium" : "low",
|
|
1214
|
-
status: "partial",
|
|
1215
|
-
basis: "scanner-rule",
|
|
1216
|
-
evidence: collectEvidence(evaluation.implementationSignals)
|
|
1217
|
-
});
|
|
1283
|
+
const outDir = args["out-dir"];
|
|
1284
|
+
if (output === "all" && outDir === void 0) {
|
|
1285
|
+
exitWithError(
|
|
1286
|
+
"--output all ben\xF6tigt --out-dir, sonst w\xFCrden zwei Formate auf stdout kollidieren."
|
|
1287
|
+
);
|
|
1218
1288
|
}
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
)
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
if (evaluation.implementationSignals.length > 0 || evaluation.supportingSignals.length === 0) {
|
|
1240
|
-
continue;
|
|
1289
|
+
const baselinePath = await resolveArtifactPath(args.baseline);
|
|
1290
|
+
const candidatePath = await resolveArtifactPath(args.candidate);
|
|
1291
|
+
const baseline = await loadBiasReport(baselinePath);
|
|
1292
|
+
const candidate = await loadBiasReport(candidatePath);
|
|
1293
|
+
const comparison = compareBiasRuns(baseline, candidate, {
|
|
1294
|
+
baselinePath,
|
|
1295
|
+
candidatePath,
|
|
1296
|
+
alpha: parseAlpha(args.alpha),
|
|
1297
|
+
topChanges: parseTop(args.top)
|
|
1298
|
+
});
|
|
1299
|
+
if (!comparison.comparable) {
|
|
1300
|
+
const message = [
|
|
1301
|
+
"Die beiden L\xE4ufe sind nicht vergleichbar \u2014 sie beantworten nicht dieselbe Frage mit demselben Messaufbau:",
|
|
1302
|
+
...comparison.comparabilityIssues.map((issue) => ` - ${issue}`)
|
|
1303
|
+
].join("\n");
|
|
1304
|
+
if (!args["allow-incomparable"]) {
|
|
1305
|
+
exitWithError(
|
|
1306
|
+
`${message}
|
|
1307
|
+
Mit --allow-incomparable wird der Vergleich trotzdem geschrieben (explizit als comparable: false markiert).`
|
|
1308
|
+
);
|
|
1241
1309
|
}
|
|
1242
|
-
|
|
1243
|
-
`[LOW] Convert documented ${COMPLIANCE_AREA_LABELS[key]} into production implementation evidence`
|
|
1244
|
-
);
|
|
1245
|
-
if (recommendations.length >= 4) break;
|
|
1310
|
+
consola4.warn(message);
|
|
1246
1311
|
}
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
const finalRecommendation = "Review the generated artifacts and complete role/risk classification in the EuConform web app";
|
|
1254
|
-
return [...recommendations.slice(0, 4), finalRecommendation];
|
|
1255
|
-
}
|
|
1256
|
-
function buildDetectedStack(signals) {
|
|
1257
|
-
const stack = [];
|
|
1258
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1259
|
-
for (const signal of signals) {
|
|
1260
|
-
if (signal.category !== "framework" && signal.category !== "runtime" && signal.category !== "ai-framework") {
|
|
1261
|
-
continue;
|
|
1312
|
+
const markdown = formatBiasComparisonMarkdown(comparison);
|
|
1313
|
+
const json = `${JSON.stringify(comparison, null, 2)}
|
|
1314
|
+
`;
|
|
1315
|
+
if (outDir === void 0) {
|
|
1316
|
+
process.stdout.write(output === "json" ? json : markdown);
|
|
1317
|
+
return;
|
|
1262
1318
|
}
|
|
1263
|
-
const
|
|
1264
|
-
|
|
1265
|
-
if (
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
return stack;
|
|
1270
|
-
}
|
|
1271
|
-
function hasCapability(group) {
|
|
1272
|
-
return group.status === "present" || group.status === "partial";
|
|
1273
|
-
}
|
|
1274
|
-
function resolveBomKind(signal, canonicalName) {
|
|
1275
|
-
if (signal.category === "rag") {
|
|
1276
|
-
return canonicalName.toLowerCase().includes("embedding") ? "embedding" : "vector-store";
|
|
1277
|
-
}
|
|
1278
|
-
return CATEGORY_TO_BOM_KIND[signal.category] ?? "tool";
|
|
1279
|
-
}
|
|
1280
|
-
function buildAiBom(scanResult, complianceAreas, generatedAt) {
|
|
1281
|
-
const components = [];
|
|
1282
|
-
const componentIndex = /* @__PURE__ */ new Map();
|
|
1283
|
-
for (const signal of scanResult.signals) {
|
|
1284
|
-
if (!BOM_CATEGORIES.has(signal.category)) continue;
|
|
1285
|
-
if (signal.category === "rag" && signal.confidence === "low") continue;
|
|
1286
|
-
const canonicalName = canonicalizeSignalName(signal);
|
|
1287
|
-
const kind = resolveBomKind(signal, canonicalName);
|
|
1288
|
-
const key = `${kind}:${canonicalName.toLowerCase()}`;
|
|
1289
|
-
const source = deriveSource(signal.evidence);
|
|
1290
|
-
const existingIndex = componentIndex.get(key);
|
|
1291
|
-
if (existingIndex === void 0) {
|
|
1292
|
-
componentIndex.set(key, components.length);
|
|
1293
|
-
components.push({
|
|
1294
|
-
id: makeBomComponentId(kind, canonicalName),
|
|
1295
|
-
kind,
|
|
1296
|
-
name: canonicalName,
|
|
1297
|
-
source
|
|
1298
|
-
});
|
|
1299
|
-
continue;
|
|
1319
|
+
const targetDir = resolve3(outDir);
|
|
1320
|
+
await mkdir2(targetDir, { recursive: true });
|
|
1321
|
+
if (output === "md" || output === "all") {
|
|
1322
|
+
const path = join(targetDir, "euconform.bias-comparison.md");
|
|
1323
|
+
await writeFile2(path, markdown);
|
|
1324
|
+
consola4.success(`Written: ${path}`);
|
|
1300
1325
|
}
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1326
|
+
if (output === "json" || output === "all") {
|
|
1327
|
+
const path = join(targetDir, "euconform.bias-comparison.json");
|
|
1328
|
+
await writeFile2(path, json);
|
|
1329
|
+
consola4.success(`Written: ${path}`);
|
|
1305
1330
|
}
|
|
1306
1331
|
}
|
|
1307
|
-
|
|
1308
|
-
return {
|
|
1309
|
-
schemaVersion: "euconform.aibom.v1",
|
|
1310
|
-
generatedAt,
|
|
1311
|
-
project: {
|
|
1312
|
-
name: scanResult.repo.name,
|
|
1313
|
-
rootPath: scanResult.meta.targetPath
|
|
1314
|
-
},
|
|
1315
|
-
components: [...components].sort(
|
|
1316
|
-
(left, right) => `${left.kind}:${left.name}`.localeCompare(`${right.kind}:${right.name}`)
|
|
1317
|
-
),
|
|
1318
|
-
complianceCapabilities: {
|
|
1319
|
-
biasEvaluation: hasCapability(complianceAreas.biasTesting.group),
|
|
1320
|
-
jsonExport: reportingSignals.some((signal) => signal.id.includes("json")),
|
|
1321
|
-
pdfExport: reportingSignals.some((signal) => signal.id.includes("pdf")),
|
|
1322
|
-
loggingInfrastructure: hasCapability(complianceAreas.loggingMonitoring.group),
|
|
1323
|
-
humanReviewFlow: hasCapability(complianceAreas.humanOversight.group),
|
|
1324
|
-
incidentHandling: hasCapability(complianceAreas.incidentReporting.group)
|
|
1325
|
-
}
|
|
1326
|
-
};
|
|
1327
|
-
}
|
|
1328
|
-
function generateScanOutput(scanResult) {
|
|
1329
|
-
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1330
|
-
const aiDetected = hasAI(scanResult.signals);
|
|
1331
|
-
const complianceAreas = evaluateComplianceAreas(scanResult.signals, aiDetected);
|
|
1332
|
-
const report = {
|
|
1333
|
-
schemaVersion: "euconform.report.v1",
|
|
1334
|
-
generatedAt,
|
|
1335
|
-
tool: {
|
|
1336
|
-
name: "euconform",
|
|
1337
|
-
version: scanResult.meta.toolVersion
|
|
1338
|
-
},
|
|
1339
|
-
target: {
|
|
1340
|
-
rootPath: scanResult.meta.targetPath,
|
|
1341
|
-
name: scanResult.repo.name,
|
|
1342
|
-
repoType: scanResult.appTypes[0] ?? "unknown",
|
|
1343
|
-
detectedStack: buildDetectedStack(scanResult.signals)
|
|
1344
|
-
},
|
|
1345
|
-
aiFootprint: buildAiFootprint(scanResult.signals),
|
|
1346
|
-
complianceSignals: {
|
|
1347
|
-
disclosure: complianceAreas.disclosure.group,
|
|
1348
|
-
biasTesting: complianceAreas.biasTesting.group,
|
|
1349
|
-
reportingExports: complianceAreas.reportingExports.group,
|
|
1350
|
-
loggingMonitoring: complianceAreas.loggingMonitoring.group,
|
|
1351
|
-
humanOversight: complianceAreas.humanOversight.group,
|
|
1352
|
-
dataGovernance: complianceAreas.dataGovernance.group,
|
|
1353
|
-
incidentReporting: complianceAreas.incidentReporting.group
|
|
1354
|
-
},
|
|
1355
|
-
assessmentHints: buildAssessmentHints2(
|
|
1356
|
-
scanResult.signals,
|
|
1357
|
-
complianceAreas,
|
|
1358
|
-
scanResult.openQuestions
|
|
1359
|
-
),
|
|
1360
|
-
gaps: buildGaps(complianceAreas, aiDetected),
|
|
1361
|
-
recommendationSummary: []
|
|
1362
|
-
};
|
|
1363
|
-
report.recommendationSummary = buildRecommendationSummary(
|
|
1364
|
-
report.gaps,
|
|
1365
|
-
scanResult.openQuestions,
|
|
1366
|
-
complianceAreas
|
|
1367
|
-
);
|
|
1368
|
-
const aibom = buildAiBom(scanResult, complianceAreas, generatedAt);
|
|
1369
|
-
const summaryMarkdown = generateSummaryMarkdown(report, aibom, scanResult.meta.scanScope);
|
|
1370
|
-
return { report, aibom, summaryMarkdown };
|
|
1371
|
-
}
|
|
1332
|
+
});
|
|
1372
1333
|
|
|
1373
|
-
//
|
|
1374
|
-
import {
|
|
1375
|
-
|
|
1376
|
-
|
|
1334
|
+
// src/commands/import.ts
|
|
1335
|
+
import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
|
|
1336
|
+
import { basename as basename2, join as join2, resolve as resolve4 } from "path";
|
|
1337
|
+
|
|
1338
|
+
// ../core/src/sbom/cyclonedx-import.ts
|
|
1339
|
+
import { basename, extname } from "path";
|
|
1340
|
+
|
|
1341
|
+
// ../core/src/sbom/known-packages.ts
|
|
1342
|
+
var KNOWN_AI_PACKAGES = {
|
|
1343
|
+
// Models / model runtimes
|
|
1344
|
+
"llama.cpp": "model",
|
|
1345
|
+
"llama-cpp-python": "model",
|
|
1346
|
+
"whisper.cpp": "model",
|
|
1347
|
+
ggml: "model",
|
|
1348
|
+
ctransformers: "model",
|
|
1349
|
+
// AI Frameworks
|
|
1350
|
+
transformers: "ai-framework",
|
|
1351
|
+
torch: "ai-framework",
|
|
1352
|
+
pytorch: "ai-framework",
|
|
1353
|
+
tensorflow: "ai-framework",
|
|
1354
|
+
"tensorflow-gpu": "ai-framework",
|
|
1355
|
+
jax: "ai-framework",
|
|
1356
|
+
keras: "ai-framework",
|
|
1357
|
+
"pytorch-lightning": "ai-framework",
|
|
1358
|
+
"scikit-learn": "ai-framework",
|
|
1359
|
+
sklearn: "ai-framework",
|
|
1360
|
+
xgboost: "ai-framework",
|
|
1361
|
+
lightgbm: "ai-framework",
|
|
1362
|
+
spacy: "ai-framework",
|
|
1363
|
+
"huggingface-hub": "ai-framework",
|
|
1364
|
+
diffusers: "ai-framework",
|
|
1365
|
+
accelerate: "ai-framework",
|
|
1366
|
+
peft: "ai-framework",
|
|
1367
|
+
trl: "ai-framework",
|
|
1368
|
+
onnxruntime: "ai-framework",
|
|
1369
|
+
"onnxruntime-gpu": "ai-framework",
|
|
1370
|
+
mlflow: "ai-framework",
|
|
1371
|
+
// Embeddings
|
|
1372
|
+
"sentence-transformers": "embedding",
|
|
1373
|
+
fastembed: "embedding",
|
|
1374
|
+
"openai-embeddings": "embedding",
|
|
1375
|
+
"cohere-embed": "embedding",
|
|
1376
|
+
// Inference Providers
|
|
1377
|
+
openai: "inference-provider",
|
|
1378
|
+
anthropic: "inference-provider",
|
|
1379
|
+
ollama: "inference-provider",
|
|
1380
|
+
vllm: "inference-provider",
|
|
1381
|
+
"together-ai": "inference-provider",
|
|
1382
|
+
replicate: "inference-provider",
|
|
1383
|
+
groq: "inference-provider",
|
|
1384
|
+
mistralai: "inference-provider",
|
|
1385
|
+
cohere: "inference-provider",
|
|
1386
|
+
"google-generativeai": "inference-provider",
|
|
1387
|
+
ai21: "inference-provider",
|
|
1388
|
+
// Vector Stores
|
|
1389
|
+
pinecone: "vector-store",
|
|
1390
|
+
"pinecone-client": "vector-store",
|
|
1391
|
+
weaviate: "vector-store",
|
|
1392
|
+
"weaviate-client": "vector-store",
|
|
1393
|
+
qdrant: "vector-store",
|
|
1394
|
+
"qdrant-client": "vector-store",
|
|
1395
|
+
milvus: "vector-store",
|
|
1396
|
+
pymilvus: "vector-store",
|
|
1397
|
+
chromadb: "vector-store",
|
|
1398
|
+
"chromadb-client": "vector-store",
|
|
1399
|
+
pgvector: "vector-store",
|
|
1400
|
+
faiss: "vector-store",
|
|
1401
|
+
"faiss-cpu": "vector-store",
|
|
1402
|
+
"faiss-gpu": "vector-store",
|
|
1403
|
+
// Datasets
|
|
1404
|
+
datasets: "dataset",
|
|
1405
|
+
// Tools / Orchestration
|
|
1406
|
+
langchain: "tool",
|
|
1407
|
+
"langchain-core": "tool",
|
|
1408
|
+
"langchain-community": "tool",
|
|
1409
|
+
llamaindex: "tool",
|
|
1410
|
+
"llama-index": "tool",
|
|
1411
|
+
"semantic-kernel": "tool",
|
|
1412
|
+
autogen: "tool",
|
|
1413
|
+
crewai: "tool",
|
|
1414
|
+
dspy: "tool",
|
|
1415
|
+
"haystack-ai": "tool",
|
|
1416
|
+
guidance: "tool",
|
|
1417
|
+
guardrails: "tool",
|
|
1418
|
+
"guardrails-ai": "tool",
|
|
1419
|
+
lmql: "tool",
|
|
1420
|
+
outlines: "tool",
|
|
1421
|
+
// Scoped packages (npm)
|
|
1422
|
+
"@langchain/core": "tool",
|
|
1423
|
+
"@langchain/openai": "tool",
|
|
1424
|
+
"@langchain/community": "tool",
|
|
1425
|
+
"@langchain/anthropic": "tool",
|
|
1426
|
+
"@huggingface/transformers": "ai-framework",
|
|
1427
|
+
"@huggingface/inference": "inference-provider",
|
|
1428
|
+
"@google/generative-ai": "inference-provider",
|
|
1429
|
+
"@anthropic-ai/sdk": "inference-provider",
|
|
1430
|
+
"@pinecone-database/pinecone": "vector-store",
|
|
1431
|
+
"@qdrant/js-client-rest": "vector-store"
|
|
1432
|
+
};
|
|
1433
|
+
var KNOWN_AI_SCOPES = {
|
|
1434
|
+
"@langchain": "tool",
|
|
1435
|
+
"@huggingface": "ai-framework",
|
|
1436
|
+
"@tensorflow": "ai-framework",
|
|
1437
|
+
"@anthropic-ai": "inference-provider",
|
|
1438
|
+
"@pinecone-database": "vector-store",
|
|
1439
|
+
"@qdrant": "vector-store"
|
|
1440
|
+
};
|
|
1441
|
+
function lookupKnownPackage(name) {
|
|
1442
|
+
const lower = name.toLowerCase();
|
|
1443
|
+
const exact = KNOWN_AI_PACKAGES[lower];
|
|
1444
|
+
if (exact) return exact;
|
|
1445
|
+
if (lower.startsWith("@") && lower.includes("/")) {
|
|
1446
|
+
const scopeEnd = lower.indexOf("/");
|
|
1447
|
+
const scope = lower.slice(0, scopeEnd);
|
|
1448
|
+
const bareName = lower.slice(scopeEnd + 1);
|
|
1449
|
+
const scopeKind = KNOWN_AI_SCOPES[scope];
|
|
1450
|
+
if (scopeKind) return scopeKind;
|
|
1451
|
+
const bareKind = KNOWN_AI_PACKAGES[bareName];
|
|
1452
|
+
if (bareKind) return bareKind;
|
|
1453
|
+
}
|
|
1454
|
+
return null;
|
|
1377
1455
|
}
|
|
1378
1456
|
|
|
1379
|
-
// ../core/src/
|
|
1380
|
-
function
|
|
1457
|
+
// ../core/src/sbom/purl.ts
|
|
1458
|
+
function parsePurl(purl) {
|
|
1459
|
+
if (!purl || !purl.startsWith("pkg:")) return null;
|
|
1460
|
+
let rest = purl.slice(4);
|
|
1461
|
+
const hashIdx = rest.indexOf("#");
|
|
1462
|
+
if (hashIdx !== -1) rest = rest.slice(0, hashIdx);
|
|
1463
|
+
const queryIdx = rest.indexOf("?");
|
|
1464
|
+
if (queryIdx !== -1) rest = rest.slice(0, queryIdx);
|
|
1465
|
+
const slashIdx = rest.indexOf("/");
|
|
1466
|
+
if (slashIdx <= 0) return null;
|
|
1467
|
+
const type = rest.slice(0, slashIdx);
|
|
1468
|
+
let remainder = rest.slice(slashIdx + 1);
|
|
1381
1469
|
try {
|
|
1382
|
-
|
|
1383
|
-
return typeof parsed.schemaVersion === "string" ? parsed.schemaVersion : void 0;
|
|
1470
|
+
remainder = decodeURIComponent(remainder);
|
|
1384
1471
|
} catch {
|
|
1385
|
-
return
|
|
1472
|
+
return null;
|
|
1386
1473
|
}
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
if (
|
|
1397
|
-
|
|
1398
|
-
|
|
1474
|
+
let version;
|
|
1475
|
+
const atIdx = remainder.lastIndexOf("@");
|
|
1476
|
+
if (atIdx !== -1) {
|
|
1477
|
+
version = remainder.slice(atIdx + 1);
|
|
1478
|
+
remainder = remainder.slice(0, atIdx);
|
|
1479
|
+
}
|
|
1480
|
+
const lastSlash = remainder.lastIndexOf("/");
|
|
1481
|
+
let namespace;
|
|
1482
|
+
let name;
|
|
1483
|
+
if (lastSlash !== -1) {
|
|
1484
|
+
namespace = remainder.slice(0, lastSlash);
|
|
1485
|
+
name = remainder.slice(lastSlash + 1);
|
|
1399
1486
|
} else {
|
|
1400
|
-
|
|
1487
|
+
name = remainder;
|
|
1401
1488
|
}
|
|
1402
|
-
return
|
|
1403
|
-
}
|
|
1404
|
-
function buildBundleManifest(opts) {
|
|
1405
|
-
const artifacts = [makeArtifactRef("report", opts.report, true)];
|
|
1406
|
-
if (opts.aibom) artifacts.push(makeArtifactRef("aibom", opts.aibom, false));
|
|
1407
|
-
if (opts.ci) artifacts.push(makeArtifactRef("ci", opts.ci, false));
|
|
1408
|
-
if (opts.summary) artifacts.push(makeArtifactRef("summary", opts.summary, false));
|
|
1489
|
+
if (!name) return null;
|
|
1409
1490
|
return {
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1491
|
+
scheme: "pkg",
|
|
1492
|
+
type,
|
|
1493
|
+
namespace: namespace || void 0,
|
|
1494
|
+
name,
|
|
1495
|
+
version: version || void 0
|
|
1415
1496
|
};
|
|
1416
1497
|
}
|
|
1417
1498
|
|
|
1418
|
-
// ../core/src/
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
}
|
|
1423
|
-
|
|
1499
|
+
// ../core/src/sbom/component-mapper.ts
|
|
1500
|
+
var CYCLONEDX_TYPE_MAP = {
|
|
1501
|
+
"machine-learning-model": { kind: "model", confidence: "high" },
|
|
1502
|
+
data: { kind: "dataset", confidence: "medium" },
|
|
1503
|
+
platform: { kind: "inference-provider", confidence: "medium" },
|
|
1504
|
+
service: { kind: "inference-provider", confidence: "medium" }
|
|
1505
|
+
};
|
|
1506
|
+
function lookupViaPurl(parsed) {
|
|
1507
|
+
if (!parsed) return null;
|
|
1508
|
+
if (parsed.namespace) {
|
|
1509
|
+
const scopedName = `${parsed.namespace}/${parsed.name}`;
|
|
1510
|
+
const scopedKind = lookupKnownPackage(scopedName);
|
|
1511
|
+
if (scopedKind) return { kind: scopedKind, confidence: "high" };
|
|
1512
|
+
}
|
|
1513
|
+
const purlKind = lookupKnownPackage(parsed.name);
|
|
1514
|
+
if (purlKind) return { kind: purlKind, confidence: "medium" };
|
|
1515
|
+
return null;
|
|
1424
1516
|
}
|
|
1425
|
-
function
|
|
1426
|
-
if (
|
|
1427
|
-
|
|
1428
|
-
`Invalid ${label} schema version: expected "${expected}", got "${String(obj.schemaVersion)}"`
|
|
1429
|
-
);
|
|
1517
|
+
function mapComponent(component) {
|
|
1518
|
+
if (!component.name || component.name.trim() === "") {
|
|
1519
|
+
return null;
|
|
1430
1520
|
}
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
if (
|
|
1434
|
-
|
|
1521
|
+
const parsed = component.purl ? parsePurl(component.purl) : null;
|
|
1522
|
+
const purlVersion = parsed?.version;
|
|
1523
|
+
if (component.modelCard != null) {
|
|
1524
|
+
return { kind: "model", source: "sbom-import", confidence: "high", purlVersion };
|
|
1435
1525
|
}
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
if (!isValid) {
|
|
1439
|
-
throw new Error(`Invalid ${label}: '${fieldName}' must be a ${type}`);
|
|
1526
|
+
if (Array.isArray(component.data) && component.data.some((d) => d.type === "dataset")) {
|
|
1527
|
+
return { kind: "dataset", source: "sbom-import", confidence: "high", purlVersion };
|
|
1440
1528
|
}
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1529
|
+
const typeMapping = CYCLONEDX_TYPE_MAP[component.type];
|
|
1530
|
+
if (typeMapping) {
|
|
1531
|
+
return {
|
|
1532
|
+
kind: typeMapping.kind,
|
|
1533
|
+
source: "sbom-import",
|
|
1534
|
+
confidence: typeMapping.confidence,
|
|
1535
|
+
purlVersion
|
|
1536
|
+
};
|
|
1447
1537
|
}
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
const match = /\.v(\d+)$/.exec(schemaVersion);
|
|
1452
|
-
return match?.[1] ?? null;
|
|
1453
|
-
}
|
|
1454
|
-
function getSchemaMajorVersion(schemaVersion) {
|
|
1455
|
-
return majorVersion(schemaVersion);
|
|
1456
|
-
}
|
|
1457
|
-
function validateScanReport(data) {
|
|
1458
|
-
const obj = assertObject(data, "report");
|
|
1459
|
-
requireSchemaVersion(obj, "euconform.report.v1", "report");
|
|
1460
|
-
requireField(obj, "generatedAt", "string", "report");
|
|
1461
|
-
const tool = requireField(obj, "tool", "object", "report");
|
|
1462
|
-
requireField(tool, "name", "string", "report", "tool.name");
|
|
1463
|
-
requireField(tool, "version", "string", "report", "tool.version");
|
|
1464
|
-
const target = requireField(obj, "target", "object", "report");
|
|
1465
|
-
requireField(target, "rootPath", "string", "report", "target.rootPath");
|
|
1466
|
-
requireField(target, "name", "string", "report", "target.name");
|
|
1467
|
-
requireField(target, "repoType", "string", "report", "target.repoType");
|
|
1468
|
-
requireField(target, "detectedStack", "array", "report", "target.detectedStack");
|
|
1469
|
-
const aiFootprint = requireField(obj, "aiFootprint", "object", "report");
|
|
1470
|
-
requireField(aiFootprint, "usesAI", "boolean", "report", "aiFootprint.usesAI");
|
|
1471
|
-
requireStringArray(aiFootprint, "inferenceModes", "report", "aiFootprint.inferenceModes");
|
|
1472
|
-
requireStringArray(aiFootprint, "providerHints", "report", "aiFootprint.providerHints");
|
|
1473
|
-
requireStringArray(aiFootprint, "ragHints", "report", "aiFootprint.ragHints");
|
|
1474
|
-
requireField(obj, "complianceSignals", "object", "report");
|
|
1475
|
-
const hints = requireField(obj, "assessmentHints", "object", "report");
|
|
1476
|
-
requireStringArray(hints, "possibleModes", "report", "assessmentHints.possibleModes");
|
|
1477
|
-
requireField(hints, "riskIndicators", "array", "report", "assessmentHints.riskIndicators");
|
|
1478
|
-
requireField(hints, "gpaiIndicators", "array", "report", "assessmentHints.gpaiIndicators");
|
|
1479
|
-
requireStringArray(hints, "openQuestions", "report", "assessmentHints.openQuestions");
|
|
1480
|
-
requireField(obj, "gaps", "array", "report");
|
|
1481
|
-
requireStringArray(obj, "recommendationSummary", "report");
|
|
1482
|
-
return data;
|
|
1483
|
-
}
|
|
1484
|
-
function validateAiBillOfMaterials(data) {
|
|
1485
|
-
const obj = assertObject(data, "AIBOM");
|
|
1486
|
-
requireSchemaVersion(obj, "euconform.aibom.v1", "AIBOM");
|
|
1487
|
-
requireField(obj, "generatedAt", "string", "AIBOM");
|
|
1488
|
-
const project = requireField(obj, "project", "object", "AIBOM");
|
|
1489
|
-
requireField(project, "name", "string", "AIBOM", "project.name");
|
|
1490
|
-
requireField(project, "rootPath", "string", "AIBOM", "project.rootPath");
|
|
1491
|
-
requireField(obj, "components", "array", "AIBOM");
|
|
1492
|
-
requireField(obj, "complianceCapabilities", "object", "AIBOM");
|
|
1493
|
-
return data;
|
|
1494
|
-
}
|
|
1495
|
-
function validateCiReport(data) {
|
|
1496
|
-
const obj = assertObject(data, "CI report");
|
|
1497
|
-
requireSchemaVersion(obj, "euconform.ci.v1", "CI report");
|
|
1498
|
-
requireField(obj, "generatedAt", "string", "CI report");
|
|
1499
|
-
const target = requireField(obj, "target", "object", "CI report");
|
|
1500
|
-
requireField(target, "name", "string", "CI report", "target.name");
|
|
1501
|
-
requireField(target, "rootPath", "string", "CI report", "target.rootPath");
|
|
1502
|
-
const status = requireField(obj, "status", "object", "CI report");
|
|
1503
|
-
requireField(status, "failOn", "string", "CI report", "status.failOn");
|
|
1504
|
-
requireField(status, "failing", "boolean", "CI report", "status.failing");
|
|
1505
|
-
requireField(status, "openQuestions", "number", "CI report", "status.openQuestions");
|
|
1506
|
-
const gapCounts = requireField(
|
|
1507
|
-
status,
|
|
1508
|
-
"gapCounts",
|
|
1509
|
-
"object",
|
|
1510
|
-
"CI report",
|
|
1511
|
-
"status.gapCounts"
|
|
1512
|
-
);
|
|
1513
|
-
for (const key of ["critical", "high", "medium", "low"]) {
|
|
1514
|
-
requireField(gapCounts, key, "number", "CI report", `status.gapCounts.${key}`);
|
|
1515
|
-
}
|
|
1516
|
-
requireField(obj, "aiDetected", "boolean", "CI report");
|
|
1517
|
-
requireField(obj, "scanScope", "string", "CI report");
|
|
1518
|
-
requireField(obj, "artifacts", "array", "CI report");
|
|
1519
|
-
requireField(obj, "complianceOverview", "array", "CI report");
|
|
1520
|
-
requireField(obj, "topGaps", "array", "CI report");
|
|
1521
|
-
return data;
|
|
1522
|
-
}
|
|
1523
|
-
var VALID_BUNDLE_ROLES = /* @__PURE__ */ new Set(["report", "aibom", "ci", "summary"]);
|
|
1524
|
-
var SHA256_HEX = /^[a-f0-9]{64}$/;
|
|
1525
|
-
function readBundleArtifactFields(raw, index) {
|
|
1526
|
-
const scope = "bundle artifact";
|
|
1527
|
-
const role = requireField(raw, "role", "string", scope, `artifacts[${index}].role`);
|
|
1528
|
-
const fileName = requireField(
|
|
1529
|
-
raw,
|
|
1530
|
-
"fileName",
|
|
1531
|
-
"string",
|
|
1532
|
-
scope,
|
|
1533
|
-
`artifacts[${index}].fileName`
|
|
1534
|
-
);
|
|
1535
|
-
const sha256 = requireField(
|
|
1536
|
-
raw,
|
|
1537
|
-
"sha256",
|
|
1538
|
-
"string",
|
|
1539
|
-
scope,
|
|
1540
|
-
`artifacts[${index}].sha256`
|
|
1541
|
-
);
|
|
1542
|
-
const required = requireField(
|
|
1543
|
-
raw,
|
|
1544
|
-
"required",
|
|
1545
|
-
"boolean",
|
|
1546
|
-
scope,
|
|
1547
|
-
`artifacts[${index}].required`
|
|
1548
|
-
);
|
|
1549
|
-
const declaredSchemaVersion = raw.schemaVersion;
|
|
1550
|
-
if (declaredSchemaVersion !== void 0 && typeof declaredSchemaVersion !== "string") {
|
|
1551
|
-
throw new Error(`Invalid bundle artifact: '${fileName}' has non-string schemaVersion`);
|
|
1538
|
+
const knownKind = lookupKnownPackage(component.name);
|
|
1539
|
+
if (knownKind) {
|
|
1540
|
+
return { kind: knownKind, source: "sbom-import", confidence: "high", purlVersion };
|
|
1552
1541
|
}
|
|
1553
|
-
|
|
1554
|
-
|
|
1542
|
+
const purlMatch = lookupViaPurl(parsed);
|
|
1543
|
+
if (purlMatch) {
|
|
1544
|
+
return {
|
|
1545
|
+
kind: purlMatch.kind,
|
|
1546
|
+
source: "sbom-import",
|
|
1547
|
+
confidence: purlMatch.confidence,
|
|
1548
|
+
purlVersion
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
return null;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
// ../core/src/sbom/types.ts
|
|
1555
|
+
var SUPPORTED_SPEC_VERSIONS = ["1.4", "1.5", "1.6"];
|
|
1556
|
+
|
|
1557
|
+
// ../core/src/sbom/cyclonedx-import.ts
|
|
1558
|
+
function importCycloneDx(sbom, options) {
|
|
1559
|
+
const bom = parseBom(sbom);
|
|
1560
|
+
const scope = options?.scope ?? "all";
|
|
1561
|
+
const allComponents = bom.components ?? [];
|
|
1562
|
+
const warnings = [];
|
|
1563
|
+
const totalComponents = allComponents.length;
|
|
1564
|
+
const inScope = scope === "production" ? allComponents.filter((c) => c.scope !== "optional" && c.scope !== "excluded") : allComponents;
|
|
1565
|
+
const filteredByScope = totalComponents - inScope.length;
|
|
1566
|
+
const mapped = [];
|
|
1567
|
+
for (const comp of inScope) {
|
|
1568
|
+
const mapping = mapComponent(comp);
|
|
1569
|
+
if (!mapping) {
|
|
1570
|
+
if (!comp.name || comp.name.trim() === "") {
|
|
1571
|
+
warnings.push({ component: "(empty)", message: "Skipped: missing or empty name" });
|
|
1572
|
+
}
|
|
1573
|
+
continue;
|
|
1574
|
+
}
|
|
1575
|
+
const version = comp.version || mapping.purlVersion;
|
|
1576
|
+
if (!version) {
|
|
1577
|
+
warnings.push({ component: comp.name, message: "Missing version field" });
|
|
1578
|
+
}
|
|
1579
|
+
mapped.push({
|
|
1580
|
+
id: version ? `${mapping.kind}:${comp.name}:${version}` : `${mapping.kind}:${comp.name}`,
|
|
1581
|
+
kind: mapping.kind,
|
|
1582
|
+
name: comp.name,
|
|
1583
|
+
...version ? { version } : {},
|
|
1584
|
+
source: "sbom-import"
|
|
1585
|
+
});
|
|
1555
1586
|
}
|
|
1587
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1588
|
+
let duplicatesRemoved = 0;
|
|
1589
|
+
const deduped = [];
|
|
1590
|
+
for (const comp of mapped) {
|
|
1591
|
+
if (seen.has(comp.id)) {
|
|
1592
|
+
duplicatesRemoved++;
|
|
1593
|
+
continue;
|
|
1594
|
+
}
|
|
1595
|
+
seen.add(comp.id);
|
|
1596
|
+
deduped.push(comp);
|
|
1597
|
+
}
|
|
1598
|
+
const byKind = {};
|
|
1599
|
+
for (const comp of deduped) {
|
|
1600
|
+
byKind[comp.kind] = (byKind[comp.kind] ?? 0) + 1;
|
|
1601
|
+
}
|
|
1602
|
+
const { projectName, projectNameSource } = resolveProjectName(bom, options?.sourcePath);
|
|
1603
|
+
const importTool = extractToolString(bom);
|
|
1604
|
+
const originalTimestamp = bom.metadata?.timestamp;
|
|
1605
|
+
const aibom = {
|
|
1606
|
+
schemaVersion: "euconform.aibom.v1.1",
|
|
1607
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1608
|
+
project: { name: projectName, rootPath: "." },
|
|
1609
|
+
components: deduped,
|
|
1610
|
+
complianceCapabilities: {
|
|
1611
|
+
biasEvaluation: false,
|
|
1612
|
+
jsonExport: false,
|
|
1613
|
+
pdfExport: false,
|
|
1614
|
+
loggingInfrastructure: false,
|
|
1615
|
+
humanReviewFlow: false,
|
|
1616
|
+
incidentHandling: false
|
|
1617
|
+
},
|
|
1618
|
+
metadata: buildMetadata(importTool, originalTimestamp)
|
|
1619
|
+
};
|
|
1620
|
+
const validation = validate(aibom);
|
|
1556
1621
|
return {
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1622
|
+
aibom,
|
|
1623
|
+
summary: {
|
|
1624
|
+
totalComponents,
|
|
1625
|
+
filteredByScope,
|
|
1626
|
+
aiRelevant: deduped.length,
|
|
1627
|
+
skipped: inScope.length - mapped.length,
|
|
1628
|
+
duplicatesRemoved,
|
|
1629
|
+
byKind,
|
|
1630
|
+
warnings,
|
|
1631
|
+
source: buildSourceInfo(bom.specVersion, projectNameSource, importTool, originalTimestamp)
|
|
1632
|
+
},
|
|
1633
|
+
validation
|
|
1563
1634
|
};
|
|
1564
1635
|
}
|
|
1565
|
-
function
|
|
1566
|
-
const
|
|
1567
|
-
if (
|
|
1568
|
-
|
|
1636
|
+
function resolveProjectName(bom, sourcePath) {
|
|
1637
|
+
const metaName = bom.metadata?.component?.name;
|
|
1638
|
+
if (metaName) {
|
|
1639
|
+
return { projectName: metaName, projectNameSource: "metadata.component.name" };
|
|
1569
1640
|
}
|
|
1570
|
-
if (
|
|
1571
|
-
|
|
1641
|
+
if (sourcePath) {
|
|
1642
|
+
const name = basename(sourcePath, extname(sourcePath));
|
|
1643
|
+
return { projectName: name, projectNameSource: "sourcePath" };
|
|
1572
1644
|
}
|
|
1573
|
-
|
|
1574
|
-
|
|
1645
|
+
return { projectName: "sbom-import", projectNameSource: "fallback" };
|
|
1646
|
+
}
|
|
1647
|
+
function extractToolString(bom) {
|
|
1648
|
+
const tool = bom.metadata?.tools?.[0];
|
|
1649
|
+
if (!tool?.name) return void 0;
|
|
1650
|
+
return tool.version ? `${tool.name} ${tool.version}` : tool.name;
|
|
1651
|
+
}
|
|
1652
|
+
function buildMetadata(importTool, originalTimestamp) {
|
|
1653
|
+
const meta = { importSource: "cyclonedx" };
|
|
1654
|
+
if (importTool) meta.importTool = importTool;
|
|
1655
|
+
if (originalTimestamp) meta.originalTimestamp = originalTimestamp;
|
|
1656
|
+
return meta;
|
|
1657
|
+
}
|
|
1658
|
+
function buildSourceInfo(specVersion, projectNameSource, importTool, originalTimestamp) {
|
|
1659
|
+
const info = { bomFormat: "CycloneDX", specVersion, projectNameSource };
|
|
1660
|
+
if (importTool) info.importTool = importTool;
|
|
1661
|
+
if (originalTimestamp) info.originalTimestamp = originalTimestamp;
|
|
1662
|
+
return info;
|
|
1663
|
+
}
|
|
1664
|
+
function parseBom(sbom) {
|
|
1665
|
+
if (typeof sbom !== "object" || sbom === null) {
|
|
1666
|
+
throw new Error("Input must be a JSON object");
|
|
1575
1667
|
}
|
|
1576
|
-
|
|
1577
|
-
|
|
1668
|
+
const record = sbom;
|
|
1669
|
+
if (record.bomFormat !== "CycloneDX") {
|
|
1670
|
+
throw new Error(`Invalid bomFormat: expected "CycloneDX", got "${String(record.bomFormat)}"`);
|
|
1578
1671
|
}
|
|
1579
|
-
|
|
1672
|
+
const specVersion = String(record.specVersion ?? "");
|
|
1673
|
+
if (!SUPPORTED_SPEC_VERSIONS.includes(specVersion)) {
|
|
1580
1674
|
throw new Error(
|
|
1581
|
-
`
|
|
1675
|
+
`Unsupported specVersion "${specVersion}". Supported: ${SUPPORTED_SPEC_VERSIONS.join(", ")}`
|
|
1582
1676
|
);
|
|
1583
1677
|
}
|
|
1678
|
+
return sbom;
|
|
1584
1679
|
}
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1680
|
+
|
|
1681
|
+
// src/commands/import.ts
|
|
1682
|
+
import { defineCommand as defineCommand3 } from "citty";
|
|
1683
|
+
import consola5 from "consola";
|
|
1684
|
+
var import_default = defineCommand3({
|
|
1685
|
+
meta: {
|
|
1686
|
+
name: "import",
|
|
1687
|
+
description: "Import a CycloneDX SBOM and extract AI-relevant components into an EuConform AI BOM"
|
|
1688
|
+
},
|
|
1689
|
+
args: {
|
|
1690
|
+
path: {
|
|
1691
|
+
type: "positional",
|
|
1692
|
+
required: true,
|
|
1693
|
+
description: "Path to a CycloneDX JSON SBOM file"
|
|
1694
|
+
},
|
|
1695
|
+
scope: {
|
|
1696
|
+
type: "string",
|
|
1697
|
+
default: "all",
|
|
1698
|
+
description: 'Component scope filter: "all" includes everything, "production" excludes optional and excluded-scope components'
|
|
1699
|
+
},
|
|
1700
|
+
json: {
|
|
1701
|
+
type: "boolean",
|
|
1702
|
+
default: false,
|
|
1703
|
+
description: "Output results as JSON to stdout"
|
|
1704
|
+
},
|
|
1705
|
+
output: {
|
|
1706
|
+
type: "string",
|
|
1707
|
+
alias: "o",
|
|
1708
|
+
default: ".euconform",
|
|
1709
|
+
description: "Output directory for the generated aibom file"
|
|
1710
|
+
}
|
|
1711
|
+
},
|
|
1712
|
+
async run({ args }) {
|
|
1713
|
+
const inputPath = resolve4(args.path);
|
|
1714
|
+
const outputDir = resolve4(args.output);
|
|
1715
|
+
const scope = args.scope === "production" ? "production" : "all";
|
|
1716
|
+
let data;
|
|
1717
|
+
try {
|
|
1718
|
+
const content = await readFile2(inputPath, "utf-8");
|
|
1719
|
+
data = JSON.parse(content);
|
|
1720
|
+
} catch {
|
|
1721
|
+
consola5.error(`Failed to read or parse file: ${inputPath}`);
|
|
1722
|
+
process.exit(2);
|
|
1723
|
+
}
|
|
1724
|
+
let result;
|
|
1725
|
+
try {
|
|
1726
|
+
result = importCycloneDx(data, { scope, sourcePath: inputPath });
|
|
1727
|
+
} catch (error) {
|
|
1728
|
+
consola5.error(error instanceof Error ? error.message : String(error));
|
|
1729
|
+
process.exit(1);
|
|
1730
|
+
}
|
|
1731
|
+
if (args.json) {
|
|
1732
|
+
process.stdout.write(
|
|
1733
|
+
`${JSON.stringify(
|
|
1734
|
+
{
|
|
1735
|
+
aibom: result.aibom,
|
|
1736
|
+
summary: result.summary,
|
|
1737
|
+
validation: { valid: result.validation.valid, errors: result.validation.errors }
|
|
1738
|
+
},
|
|
1739
|
+
null,
|
|
1740
|
+
2
|
|
1741
|
+
)}
|
|
1742
|
+
`
|
|
1743
|
+
);
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
const fileName = basename2(inputPath);
|
|
1747
|
+
const src = result.summary.source;
|
|
1748
|
+
const sourceLabel = src.importTool ? `${fileName} (CycloneDX ${src.specVersion}, ${src.importTool})` : `${fileName} (CycloneDX ${src.specVersion})`;
|
|
1749
|
+
consola5.log("");
|
|
1750
|
+
consola5.log("CycloneDX Import");
|
|
1751
|
+
consola5.log("\u2500".repeat(40));
|
|
1752
|
+
consola5.log(`Source: ${sourceLabel}`);
|
|
1753
|
+
consola5.log(`Project: ${result.aibom.project.name}`);
|
|
1754
|
+
let componentsLine = `Components: ${result.summary.totalComponents} total \u2192 ${result.summary.aiRelevant} AI-relevant, ${result.summary.skipped} skipped`;
|
|
1755
|
+
if (result.summary.filteredByScope > 0) {
|
|
1756
|
+
componentsLine += `, ${result.summary.filteredByScope} scope-filtered`;
|
|
1757
|
+
}
|
|
1758
|
+
if (result.summary.duplicatesRemoved > 0) {
|
|
1759
|
+
componentsLine += `, ${result.summary.duplicatesRemoved} duplicates removed`;
|
|
1760
|
+
}
|
|
1761
|
+
consola5.log(componentsLine);
|
|
1762
|
+
consola5.log("");
|
|
1763
|
+
for (const [kind, count] of Object.entries(result.summary.byKind).sort((a, b) => b[1] - a[1])) {
|
|
1764
|
+
const names = result.aibom.components.filter((c) => c.kind === kind).map((c) => c.name).join(", ");
|
|
1765
|
+
consola5.log(` ${kind.padEnd(20)} ${String(count).padStart(2)} (${names})`);
|
|
1766
|
+
}
|
|
1767
|
+
if (result.summary.warnings.length > 0) {
|
|
1768
|
+
consola5.log("");
|
|
1769
|
+
for (const w of result.summary.warnings) {
|
|
1770
|
+
consola5.warn(`${w.component}: ${w.message}`);
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
await mkdir3(outputDir, { recursive: true });
|
|
1774
|
+
const aibomPath = join2(outputDir, "euconform.aibom.json");
|
|
1775
|
+
await writeFile3(aibomPath, JSON.stringify(result.aibom, null, 2), "utf-8");
|
|
1776
|
+
consola5.log("");
|
|
1777
|
+
consola5.success(`${aibomPath} ${result.validation.valid ? "(valid)" : "(validation errors!)"}`);
|
|
1778
|
+
if (!result.validation.valid) {
|
|
1779
|
+
for (const err of result.validation.errors) {
|
|
1780
|
+
consola5.error(` ${err.path || "/"} \u2014 ${err.message}`);
|
|
1781
|
+
}
|
|
1782
|
+
process.exit(1);
|
|
1614
1783
|
}
|
|
1615
1784
|
}
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
}
|
|
1785
|
+
});
|
|
1786
|
+
|
|
1787
|
+
// src/commands/scan.ts
|
|
1788
|
+
import { stat as stat4 } from "fs/promises";
|
|
1789
|
+
import { resolve as resolve6 } from "path";
|
|
1621
1790
|
|
|
1622
1791
|
// ../core/src/scanner/aggregator.ts
|
|
1623
|
-
import { basename as
|
|
1792
|
+
import { basename as basename5 } from "path";
|
|
1624
1793
|
|
|
1625
1794
|
// ../core/src/scanner/detectors/shared.ts
|
|
1626
1795
|
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
@@ -1665,7 +1834,7 @@ var FRAMEWORK_IMPORTS = [
|
|
|
1665
1834
|
{
|
|
1666
1835
|
id: "ai-framework-transformers",
|
|
1667
1836
|
name: "Transformers.js",
|
|
1668
|
-
importPattern: /(?:from\s+['"]
|
|
1837
|
+
importPattern: /(?:from\s+['"](?:@xenova|@huggingface)\/transformers['"]|require\s*\(\s*['"](?:@xenova|@huggingface)\/transformers['"])/
|
|
1669
1838
|
},
|
|
1670
1839
|
{
|
|
1671
1840
|
id: "ai-framework-vercel-ai",
|
|
@@ -2130,16 +2299,16 @@ var dockerfileRules = [
|
|
|
2130
2299
|
}
|
|
2131
2300
|
];
|
|
2132
2301
|
function isEnvExample(relativePath) {
|
|
2133
|
-
const
|
|
2134
|
-
return /^\.env\.(?:example|sample)$/.test(
|
|
2302
|
+
const basename7 = relativePath.split("/").pop() ?? "";
|
|
2303
|
+
return /^\.env\.(?:example|sample)$/.test(basename7);
|
|
2135
2304
|
}
|
|
2136
2305
|
function isDockerCompose(relativePath) {
|
|
2137
|
-
const
|
|
2138
|
-
return /^docker-compose/.test(
|
|
2306
|
+
const basename7 = relativePath.split("/").pop() ?? "";
|
|
2307
|
+
return /^docker-compose/.test(basename7);
|
|
2139
2308
|
}
|
|
2140
2309
|
function isDockerfile(relativePath) {
|
|
2141
|
-
const
|
|
2142
|
-
return /^Dockerfile/.test(
|
|
2310
|
+
const basename7 = relativePath.split("/").pop() ?? "";
|
|
2311
|
+
return /^Dockerfile/.test(basename7);
|
|
2143
2312
|
}
|
|
2144
2313
|
function detectConfig(ctx) {
|
|
2145
2314
|
const { file } = ctx;
|
|
@@ -2320,8 +2489,8 @@ var rules2 = [
|
|
|
2320
2489
|
var CONFIG_NAME_PATTERN = /^(next|vite)\.config\./;
|
|
2321
2490
|
function shouldProcess(file) {
|
|
2322
2491
|
if (EXTENDED_EXTENSIONS.has(file.extension)) return true;
|
|
2323
|
-
const
|
|
2324
|
-
return CONFIG_NAME_PATTERN.test(
|
|
2492
|
+
const basename7 = file.relativePath.split("/").pop() ?? "";
|
|
2493
|
+
return CONFIG_NAME_PATTERN.test(basename7);
|
|
2325
2494
|
}
|
|
2326
2495
|
function findImportEvidence(lines, pattern, filePath) {
|
|
2327
2496
|
const evidence = [];
|
|
@@ -2338,11 +2507,11 @@ function findImportEvidence(lines, pattern, filePath) {
|
|
|
2338
2507
|
function detectFrameworks(ctx) {
|
|
2339
2508
|
const { file } = ctx;
|
|
2340
2509
|
if (!shouldProcess(file)) return [];
|
|
2341
|
-
const
|
|
2510
|
+
const basename7 = file.relativePath.split("/").pop() ?? "";
|
|
2342
2511
|
const seen = /* @__PURE__ */ new Set();
|
|
2343
2512
|
const signals = [];
|
|
2344
2513
|
for (const rule of rules2) {
|
|
2345
|
-
if (rule.configPattern?.test(
|
|
2514
|
+
if (rule.configPattern?.test(basename7) && !seen.has(rule.name)) {
|
|
2346
2515
|
seen.add(rule.name);
|
|
2347
2516
|
signals.push({
|
|
2348
2517
|
id: `${rule.category}-${rule.name}`,
|
|
@@ -2350,7 +2519,7 @@ function detectFrameworks(ctx) {
|
|
|
2350
2519
|
category: rule.category,
|
|
2351
2520
|
confidence: rule.configConfidence,
|
|
2352
2521
|
evidence: [
|
|
2353
|
-
{ file: file.relativePath, line: 1, snippet: `Config file detected: ${
|
|
2522
|
+
{ file: file.relativePath, line: 1, snippet: `Config file detected: ${basename7}` }
|
|
2354
2523
|
]
|
|
2355
2524
|
});
|
|
2356
2525
|
}
|
|
@@ -2391,7 +2560,7 @@ var RULES = [
|
|
|
2391
2560
|
{
|
|
2392
2561
|
id: "local-inference-transformers-browser",
|
|
2393
2562
|
name: "Transformers.js (browser inference)",
|
|
2394
|
-
pattern: /(?:from\s+['"]
|
|
2563
|
+
pattern: /(?:from\s+['"](?:@xenova|@huggingface)\/transformers['"]|\.pipeline\s*\(|env\.allowLocalModels)/,
|
|
2395
2564
|
confidence: "medium",
|
|
2396
2565
|
extensions: [".ts", ".tsx", ".js", ".jsx"]
|
|
2397
2566
|
},
|
|
@@ -2523,12 +2692,12 @@ var SKIP_BASENAMES = /* @__PURE__ */ new Set([
|
|
|
2523
2692
|
]);
|
|
2524
2693
|
function isRelevantMarkdown(relativePath) {
|
|
2525
2694
|
const lower = relativePath.toLowerCase();
|
|
2526
|
-
const
|
|
2527
|
-
if (
|
|
2695
|
+
const basename7 = lower.split("/").pop() ?? "";
|
|
2696
|
+
if (basename7.startsWith("readme")) return true;
|
|
2528
2697
|
if (lower.includes("/docs/") || lower.startsWith("docs/")) {
|
|
2529
|
-
return !SKIP_BASENAMES.has(
|
|
2698
|
+
return !SKIP_BASENAMES.has(basename7);
|
|
2530
2699
|
}
|
|
2531
|
-
if (SKIP_BASENAMES.has(
|
|
2700
|
+
if (SKIP_BASENAMES.has(basename7)) return false;
|
|
2532
2701
|
return true;
|
|
2533
2702
|
}
|
|
2534
2703
|
function gatherMarkdownEvidence(rule, lines, filePath) {
|
|
@@ -2544,8 +2713,8 @@ function gatherMarkdownEvidence(rule, lines, filePath) {
|
|
|
2544
2713
|
}
|
|
2545
2714
|
return evidence;
|
|
2546
2715
|
}
|
|
2547
|
-
function isSkippableGenericContent(
|
|
2548
|
-
if (!SKIP_BASENAMES.has(
|
|
2716
|
+
function isSkippableGenericContent(basename7, evidence) {
|
|
2717
|
+
if (!SKIP_BASENAMES.has(basename7)) return false;
|
|
2549
2718
|
return !evidence.some((e) => /\bAI\b|\bML\b|\bmodel\b|\binference\b/i.test(e.snippet));
|
|
2550
2719
|
}
|
|
2551
2720
|
function detectMarkdown(ctx) {
|
|
@@ -2553,11 +2722,11 @@ function detectMarkdown(ctx) {
|
|
|
2553
2722
|
if (file.extension !== ".md") return [];
|
|
2554
2723
|
if (!isRelevantMarkdown(file.relativePath)) return [];
|
|
2555
2724
|
const signalMap = /* @__PURE__ */ new Map();
|
|
2556
|
-
const
|
|
2725
|
+
const basename7 = file.relativePath.toLowerCase().split("/").pop() ?? "";
|
|
2557
2726
|
for (const rule of rules3) {
|
|
2558
2727
|
const evidence = gatherMarkdownEvidence(rule, ctx.lines, file.relativePath);
|
|
2559
2728
|
if (evidence.length === 0) continue;
|
|
2560
|
-
if (isSkippableGenericContent(
|
|
2729
|
+
if (isSkippableGenericContent(basename7, evidence)) continue;
|
|
2561
2730
|
const existing = signalMap.get(rule.id);
|
|
2562
2731
|
if (existing) {
|
|
2563
2732
|
existing.evidence.push(...evidence);
|
|
@@ -2575,7 +2744,7 @@ function detectMarkdown(ctx) {
|
|
|
2575
2744
|
}
|
|
2576
2745
|
|
|
2577
2746
|
// ../core/src/scanner/detectors/package-json-detector.ts
|
|
2578
|
-
import { basename } from "path";
|
|
2747
|
+
import { basename as basename3 } from "path";
|
|
2579
2748
|
var AI_PACKAGES = {
|
|
2580
2749
|
// Providers
|
|
2581
2750
|
openai: { category: "ai-provider", label: "OpenAI SDK" },
|
|
@@ -2588,6 +2757,7 @@ var AI_PACKAGES = {
|
|
|
2588
2757
|
"@langchain/core": { category: "ai-framework", label: "LangChain Core" },
|
|
2589
2758
|
llamaindex: { category: "ai-framework", label: "LlamaIndex" },
|
|
2590
2759
|
"@xenova/transformers": { category: "ai-framework", label: "Transformers.js" },
|
|
2760
|
+
"@huggingface/transformers": { category: "ai-framework", label: "Transformers.js" },
|
|
2591
2761
|
ai: { category: "ai-framework", label: "Vercel AI SDK" },
|
|
2592
2762
|
"@huggingface/inference": { category: "ai-framework", label: "HuggingFace Inference" },
|
|
2593
2763
|
// Local inference
|
|
@@ -2622,12 +2792,12 @@ var PACKAGE_JSON_NAMES = /* @__PURE__ */ new Set([
|
|
|
2622
2792
|
"yarn.lock"
|
|
2623
2793
|
]);
|
|
2624
2794
|
function isPackageJsonFile(relativePath) {
|
|
2625
|
-
return PACKAGE_JSON_NAMES.has(
|
|
2795
|
+
return PACKAGE_JSON_NAMES.has(basename3(relativePath));
|
|
2626
2796
|
}
|
|
2627
2797
|
function detectPackageJsonSignals(ctx) {
|
|
2628
2798
|
const { file } = ctx;
|
|
2629
2799
|
if (!isPackageJsonFile(file.relativePath)) return [];
|
|
2630
|
-
const fileName =
|
|
2800
|
+
const fileName = basename3(file.relativePath);
|
|
2631
2801
|
if (fileName === "package.json") {
|
|
2632
2802
|
return detectFromPackageJson(file.relativePath, file.content);
|
|
2633
2803
|
}
|
|
@@ -2868,8 +3038,8 @@ function runAllDetectors(ctx) {
|
|
|
2868
3038
|
|
|
2869
3039
|
// ../core/src/scanner/discovery.ts
|
|
2870
3040
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
2871
|
-
import { readFile, readdir, stat as
|
|
2872
|
-
import { basename as
|
|
3041
|
+
import { readFile as readFile3, readdir, stat as stat3 } from "fs/promises";
|
|
3042
|
+
import { basename as basename4, extname as extname2, join as join3, resolve as resolve5 } from "path";
|
|
2873
3043
|
var DEFAULT_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".json", ".md", ".yaml", ".yml"];
|
|
2874
3044
|
var NAMED_FILE_PATTERNS = [
|
|
2875
3045
|
/^package\.json$/,
|
|
@@ -2965,7 +3135,7 @@ function classifyFileProvenance(relativePath) {
|
|
|
2965
3135
|
const segments = rawSegments.map((segment) => segment.toLowerCase());
|
|
2966
3136
|
const rawFileName = rawSegments[rawSegments.length - 1] ?? "";
|
|
2967
3137
|
const fileName = segments[segments.length - 1] ?? "";
|
|
2968
|
-
const ext =
|
|
3138
|
+
const ext = extname2(fileName);
|
|
2969
3139
|
const isRootFile = segments.length === 1;
|
|
2970
3140
|
if (segments.some((segment) => FIXTURE_SEGMENTS.has(segment))) return "fixture";
|
|
2971
3141
|
if (segments.some((segment) => TEST_SEGMENTS.has(segment)) || TEST_FILE_PATTERN.test(fileName)) {
|
|
@@ -2986,7 +3156,7 @@ function classifyFileProvenance(relativePath) {
|
|
|
2986
3156
|
async function loadGitignore(targetPath) {
|
|
2987
3157
|
const ig = (0, import_ignore.default)();
|
|
2988
3158
|
try {
|
|
2989
|
-
const content = await
|
|
3159
|
+
const content = await readFile3(join3(targetPath, ".gitignore"), "utf-8");
|
|
2990
3160
|
ig.add(content);
|
|
2991
3161
|
} catch {
|
|
2992
3162
|
}
|
|
@@ -2994,9 +3164,9 @@ async function loadGitignore(targetPath) {
|
|
|
2994
3164
|
return ig;
|
|
2995
3165
|
}
|
|
2996
3166
|
function shouldProcessEntry(entry, targetPath, filterOpts) {
|
|
2997
|
-
const absolutePath =
|
|
2998
|
-
const fileName =
|
|
2999
|
-
const ext =
|
|
3167
|
+
const absolutePath = join3(targetPath, entry);
|
|
3168
|
+
const fileName = basename4(entry);
|
|
3169
|
+
const ext = extname2(fileName).toLowerCase();
|
|
3000
3170
|
const provenance = classifyFileProvenance(entry);
|
|
3001
3171
|
if (filterOpts.scope === "production" && PRODUCTION_SKIP_PROVENANCE.has(provenance)) {
|
|
3002
3172
|
return "skip";
|
|
@@ -3006,10 +3176,10 @@ function shouldProcessEntry(entry, targetPath, filterOpts) {
|
|
|
3006
3176
|
}
|
|
3007
3177
|
async function tryReadFile(absolutePath, entry, ext, provenance, maxSize) {
|
|
3008
3178
|
try {
|
|
3009
|
-
const fileStat = await
|
|
3179
|
+
const fileStat = await stat3(absolutePath);
|
|
3010
3180
|
if (!fileStat.isFile() || fileStat.size === 0) return "ignore";
|
|
3011
3181
|
if (fileStat.size > maxSize) return "skip";
|
|
3012
|
-
const content = await
|
|
3182
|
+
const content = await readFile3(absolutePath, "utf-8");
|
|
3013
3183
|
return {
|
|
3014
3184
|
relativePath: entry,
|
|
3015
3185
|
absolutePath,
|
|
@@ -3024,7 +3194,7 @@ async function tryReadFile(absolutePath, entry, ext, provenance, maxSize) {
|
|
|
3024
3194
|
}
|
|
3025
3195
|
function resolveOptions(options) {
|
|
3026
3196
|
return {
|
|
3027
|
-
targetPath:
|
|
3197
|
+
targetPath: resolve5(options.targetPath),
|
|
3028
3198
|
scope: options.scope ?? DEFAULT_SCOPE,
|
|
3029
3199
|
extensions: options.extensions ?? DEFAULT_EXTENSIONS,
|
|
3030
3200
|
maxSize: options.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE,
|
|
@@ -3104,7 +3274,7 @@ function buildRepoProfile(targetPath, pkg, files) {
|
|
|
3104
3274
|
pkg?.workspaces || files.some((f) => f.relativePath === "pnpm-workspace.yaml")
|
|
3105
3275
|
);
|
|
3106
3276
|
return {
|
|
3107
|
-
name: pkg?.name ??
|
|
3277
|
+
name: pkg?.name ?? basename5(targetPath),
|
|
3108
3278
|
version: pkg?.version,
|
|
3109
3279
|
description: pkg?.description,
|
|
3110
3280
|
license: pkg?.license,
|
|
@@ -3164,10 +3334,10 @@ function annotateEvidenceProvenance(signals, files) {
|
|
|
3164
3334
|
}
|
|
3165
3335
|
function generateOpenQuestions(signals) {
|
|
3166
3336
|
const questions = [];
|
|
3167
|
-
const
|
|
3337
|
+
const hasAI = signals.some(
|
|
3168
3338
|
(s) => s.category === "ai-provider" || s.category === "ai-framework" || s.category === "local-inference"
|
|
3169
3339
|
);
|
|
3170
|
-
if (
|
|
3340
|
+
if (hasAI) {
|
|
3171
3341
|
questions.push({
|
|
3172
3342
|
id: "oq-market-placement",
|
|
3173
3343
|
question: "Is this AI system placed on the EU market or used within the EU?",
|
|
@@ -3203,7 +3373,7 @@ function generateOpenQuestions(signals) {
|
|
|
3203
3373
|
});
|
|
3204
3374
|
}
|
|
3205
3375
|
const hasNoDisclosure = !hasImplementationSignals(signals, ["compliance-disclosure"]);
|
|
3206
|
-
if (
|
|
3376
|
+
if (hasAI && hasNoDisclosure) {
|
|
3207
3377
|
questions.push({
|
|
3208
3378
|
id: "oq-disclosure-missing",
|
|
3209
3379
|
question: "Does your system inform users that they are interacting with AI?",
|
|
@@ -3259,13 +3429,13 @@ async function scanRepository(options) {
|
|
|
3259
3429
|
}
|
|
3260
3430
|
|
|
3261
3431
|
// src/commands/scan.ts
|
|
3262
|
-
import { defineCommand as
|
|
3263
|
-
import
|
|
3432
|
+
import { defineCommand as defineCommand4 } from "citty";
|
|
3433
|
+
import consola8 from "consola";
|
|
3264
3434
|
|
|
3265
3435
|
// src/output/ci.ts
|
|
3266
|
-
import { appendFile, mkdir as
|
|
3267
|
-
import { join as
|
|
3268
|
-
import
|
|
3436
|
+
import { appendFile, mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
|
|
3437
|
+
import { join as join4 } from "path";
|
|
3438
|
+
import consola6 from "consola";
|
|
3269
3439
|
|
|
3270
3440
|
// src/utils/gap-priority.ts
|
|
3271
3441
|
var GAP_PRIORITY_ORDER = {
|
|
@@ -3416,15 +3586,15 @@ function emitGitHubAnnotations(output) {
|
|
|
3416
3586
|
}
|
|
3417
3587
|
async function writeCiArtifacts(output, outputDir, failOn, mode, scanScope, baseArtifacts) {
|
|
3418
3588
|
if (mode === "off") return null;
|
|
3419
|
-
await
|
|
3589
|
+
await mkdir4(outputDir, { recursive: true });
|
|
3420
3590
|
const ciReport = buildCiReport(output, failOn, scanScope, baseArtifacts);
|
|
3421
|
-
const reportPath =
|
|
3422
|
-
const summaryPath =
|
|
3423
|
-
await
|
|
3591
|
+
const reportPath = join4(outputDir, "euconform.ci.json");
|
|
3592
|
+
const summaryPath = join4(outputDir, "euconform.ci-summary.md");
|
|
3593
|
+
await writeFile4(reportPath, `${JSON.stringify(ciReport, null, 2)}
|
|
3424
3594
|
`, "utf-8");
|
|
3425
|
-
await
|
|
3426
|
-
|
|
3427
|
-
|
|
3595
|
+
await writeFile4(summaryPath, renderCiSummaryMarkdown(output, ciReport), "utf-8");
|
|
3596
|
+
consola6.success(`Written ${reportPath}`);
|
|
3597
|
+
consola6.success(`Written ${summaryPath}`);
|
|
3428
3598
|
let githubStepSummaryPath;
|
|
3429
3599
|
if (mode === "github") {
|
|
3430
3600
|
emitGitHubAnnotations(output);
|
|
@@ -3433,14 +3603,14 @@ async function writeCiArtifacts(output, outputDir, failOn, mode, scanScope, base
|
|
|
3433
3603
|
await appendFile(stepSummaryPath, `
|
|
3434
3604
|
${renderCiSummaryMarkdown(output, ciReport)}`, "utf-8");
|
|
3435
3605
|
githubStepSummaryPath = stepSummaryPath;
|
|
3436
|
-
|
|
3606
|
+
consola6.info(`Appended GitHub step summary: ${stepSummaryPath}`);
|
|
3437
3607
|
}
|
|
3438
3608
|
}
|
|
3439
3609
|
return { reportPath, summaryPath, githubStepSummaryPath, baseArtifacts };
|
|
3440
3610
|
}
|
|
3441
3611
|
|
|
3442
3612
|
// src/output/terminal.ts
|
|
3443
|
-
import
|
|
3613
|
+
import consola7 from "consola";
|
|
3444
3614
|
var AREA_LABELS = {
|
|
3445
3615
|
disclosure: "Disclosure & Transparency",
|
|
3446
3616
|
biasTesting: "Bias Testing",
|
|
@@ -3461,142 +3631,50 @@ function printSignalCounts(complianceSignals) {
|
|
|
3461
3631
|
byConfidence[area.confidence]++;
|
|
3462
3632
|
}
|
|
3463
3633
|
const total = Object.values(complianceSignals).length;
|
|
3464
|
-
|
|
3634
|
+
consola7.info(
|
|
3465
3635
|
`${total} compliance areas assessed (${byConfidence.high} high, ${byConfidence.medium} medium, ${byConfidence.low} low confidence)`
|
|
3466
3636
|
);
|
|
3467
3637
|
}
|
|
3468
3638
|
function printComplianceOverview(complianceSignals) {
|
|
3469
|
-
|
|
3470
|
-
|
|
3639
|
+
consola7.log("");
|
|
3640
|
+
consola7.info("Compliance Overview:");
|
|
3471
3641
|
for (const [key, group] of Object.entries(complianceSignals)) {
|
|
3472
3642
|
const label = AREA_LABELS[key] ?? key;
|
|
3473
3643
|
const icon = STATUS_ICONS[group.status] ?? "[??]";
|
|
3474
|
-
|
|
3644
|
+
consola7.log(` ${icon} ${label.padEnd(28)} ${group.status} (${group.confidence})`);
|
|
3475
3645
|
}
|
|
3476
3646
|
}
|
|
3477
3647
|
function printTopGaps(gaps) {
|
|
3478
3648
|
if (gaps.length === 0) return;
|
|
3479
|
-
|
|
3480
|
-
|
|
3649
|
+
consola7.log("");
|
|
3650
|
+
consola7.warn(`Top gaps (${gaps.length} total):`);
|
|
3481
3651
|
for (const gap of gaps.slice(0, 3)) {
|
|
3482
|
-
|
|
3652
|
+
consola7.log(` - [${gap.priority}] ${gap.title}`);
|
|
3483
3653
|
}
|
|
3484
3654
|
if (gaps.length > 3) {
|
|
3485
|
-
|
|
3655
|
+
consola7.log(` ... and ${gaps.length - 3} more`);
|
|
3486
3656
|
}
|
|
3487
3657
|
}
|
|
3488
3658
|
function printTerminalSummary(output, outputDir) {
|
|
3489
3659
|
const { report } = output;
|
|
3490
|
-
|
|
3660
|
+
consola7.box("EuConform Scan Complete");
|
|
3491
3661
|
printSignalCounts(report.complianceSignals);
|
|
3492
3662
|
const ai = report.aiFootprint;
|
|
3493
|
-
|
|
3663
|
+
consola7.info(
|
|
3494
3664
|
`AI detected: ${ai.usesAI ? "yes" : "no"}${ai.inferenceModes.length > 0 ? ` | Inference: ${ai.inferenceModes.join(", ")}` : ""}`
|
|
3495
3665
|
);
|
|
3496
3666
|
printComplianceOverview(report.complianceSignals);
|
|
3497
3667
|
printTopGaps(report.gaps);
|
|
3498
3668
|
const openCount = report.assessmentHints.openQuestions.length;
|
|
3499
3669
|
if (openCount > 0) {
|
|
3500
|
-
|
|
3501
|
-
}
|
|
3502
|
-
consola4.log("");
|
|
3503
|
-
consola4.success(`Output written to: ${outputDir}`);
|
|
3504
|
-
consola4.info("Next steps:");
|
|
3505
|
-
consola4.log(" - Review `euconform.report.json` and `euconform.summary.md`");
|
|
3506
|
-
consola4.log(" - Complete role and risk classification in the EuConform web app");
|
|
3507
|
-
consola4.log(" - Use `--ci github --fail-on <level>` to turn scans into CI checks");
|
|
3508
|
-
}
|
|
3509
|
-
|
|
3510
|
-
// src/output/writer.ts
|
|
3511
|
-
import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
|
|
3512
|
-
import { join as join3 } from "path";
|
|
3513
|
-
import consola5 from "consola";
|
|
3514
|
-
async function tryReadArtifact(outputDir, fileName) {
|
|
3515
|
-
try {
|
|
3516
|
-
const content = await readFile2(join3(outputDir, fileName), "utf-8");
|
|
3517
|
-
return { fileName, content };
|
|
3518
|
-
} catch {
|
|
3519
|
-
return null;
|
|
3520
|
-
}
|
|
3521
|
-
}
|
|
3522
|
-
async function writeBundleManifest(outputDir, metadata) {
|
|
3523
|
-
const report = await tryReadArtifact(outputDir, "euconform.report.json");
|
|
3524
|
-
if (!report) {
|
|
3525
|
-
return null;
|
|
3526
|
-
}
|
|
3527
|
-
const bundle = buildBundleManifest({
|
|
3528
|
-
report,
|
|
3529
|
-
aibom: await tryReadArtifact(outputDir, "euconform.aibom.json") ?? void 0,
|
|
3530
|
-
ci: await tryReadArtifact(outputDir, "euconform.ci.json") ?? void 0,
|
|
3531
|
-
summary: await tryReadArtifact(outputDir, "euconform.summary.md") ?? void 0,
|
|
3532
|
-
tool: metadata.tool,
|
|
3533
|
-
target: metadata.target,
|
|
3534
|
-
generatedAt: metadata.generatedAt
|
|
3535
|
-
});
|
|
3536
|
-
const bundlePath = join3(outputDir, "euconform.bundle.json");
|
|
3537
|
-
await writeFile3(bundlePath, `${JSON.stringify(bundle, null, 2)}
|
|
3538
|
-
`, "utf-8");
|
|
3539
|
-
consola5.success(`Written ${bundlePath}`);
|
|
3540
|
-
return bundlePath;
|
|
3541
|
-
}
|
|
3542
|
-
async function writeOutputFiles(output, outputDir, format) {
|
|
3543
|
-
await mkdir3(outputDir, { recursive: true });
|
|
3544
|
-
const written = [];
|
|
3545
|
-
if (format === "json" || format === "all") {
|
|
3546
|
-
const reportContent = JSON.stringify(output.report, null, 2);
|
|
3547
|
-
const reportPath = join3(outputDir, "euconform.report.json");
|
|
3548
|
-
await writeFile3(reportPath, reportContent, "utf-8");
|
|
3549
|
-
consola5.success(`Written ${reportPath}`);
|
|
3550
|
-
written.push({ fileName: "euconform.report.json", content: reportContent });
|
|
3551
|
-
const aibomContent = JSON.stringify(output.aibom, null, 2);
|
|
3552
|
-
const aibomPath = join3(outputDir, "euconform.aibom.json");
|
|
3553
|
-
await writeFile3(aibomPath, aibomContent, "utf-8");
|
|
3554
|
-
consola5.success(`Written ${aibomPath}`);
|
|
3555
|
-
written.push({ fileName: "euconform.aibom.json", content: aibomContent });
|
|
3670
|
+
consola7.info(`${openCount} open question${openCount > 1 ? "s" : ""} for manual review`);
|
|
3556
3671
|
}
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
if (written.some((artifact) => artifact.fileName === "euconform.report.json")) {
|
|
3564
|
-
await writeBundleManifest(outputDir, {
|
|
3565
|
-
tool: output.report.tool,
|
|
3566
|
-
target: {
|
|
3567
|
-
name: output.report.target.name,
|
|
3568
|
-
rootPath: output.report.target.rootPath
|
|
3569
|
-
},
|
|
3570
|
-
generatedAt: output.report.generatedAt
|
|
3571
|
-
});
|
|
3572
|
-
}
|
|
3573
|
-
}
|
|
3574
|
-
async function writeZipBundle(outputDir) {
|
|
3575
|
-
const { zipSync, strToU8 } = await import("fflate");
|
|
3576
|
-
const fileNames = [
|
|
3577
|
-
"euconform.report.json",
|
|
3578
|
-
"euconform.aibom.json",
|
|
3579
|
-
"euconform.summary.md",
|
|
3580
|
-
"euconform.ci.json",
|
|
3581
|
-
"euconform.ci-summary.md",
|
|
3582
|
-
"euconform.bundle.json"
|
|
3583
|
-
];
|
|
3584
|
-
const zipData = {};
|
|
3585
|
-
for (const fileName of fileNames) {
|
|
3586
|
-
try {
|
|
3587
|
-
const content = await readFile2(join3(outputDir, fileName), "utf-8");
|
|
3588
|
-
zipData[fileName] = strToU8(content);
|
|
3589
|
-
} catch {
|
|
3590
|
-
}
|
|
3591
|
-
}
|
|
3592
|
-
if (!("euconform.bundle.json" in zipData)) {
|
|
3593
|
-
throw new Error("Cannot create euconform.bundle.zip without euconform.bundle.json");
|
|
3594
|
-
}
|
|
3595
|
-
const zipped = zipSync(zipData);
|
|
3596
|
-
const zipPath = join3(outputDir, "euconform.bundle.zip");
|
|
3597
|
-
await writeFile3(zipPath, zipped);
|
|
3598
|
-
consola5.success(`Written ${zipPath}`);
|
|
3599
|
-
return zipPath;
|
|
3672
|
+
consola7.log("");
|
|
3673
|
+
consola7.success(`Output written to: ${outputDir}`);
|
|
3674
|
+
consola7.info("Next steps:");
|
|
3675
|
+
consola7.log(" - Review `euconform.report.json` and `euconform.summary.md`");
|
|
3676
|
+
consola7.log(" - Complete role and risk classification in the EuConform web app");
|
|
3677
|
+
consola7.log(" - Use `--ci github --fail-on <level>` to turn scans into CI checks");
|
|
3600
3678
|
}
|
|
3601
3679
|
|
|
3602
3680
|
// src/commands/scan.ts
|
|
@@ -3606,7 +3684,7 @@ var FAIL_ON_LEVELS = ["none", "critical", "high", "medium", "low"];
|
|
|
3606
3684
|
var VALID_CI_MODES = /* @__PURE__ */ new Set(["off", "github"]);
|
|
3607
3685
|
async function assertDirectoryExists(targetPath) {
|
|
3608
3686
|
try {
|
|
3609
|
-
const info = await
|
|
3687
|
+
const info = await stat4(targetPath);
|
|
3610
3688
|
if (!info.isDirectory()) {
|
|
3611
3689
|
exitWithError(`Not a directory: ${targetPath}`);
|
|
3612
3690
|
}
|
|
@@ -3638,7 +3716,7 @@ function parseExcludeGlobs(raw) {
|
|
|
3638
3716
|
return Array.isArray(raw) ? raw : [raw];
|
|
3639
3717
|
}
|
|
3640
3718
|
async function validateAndParseArgs(args) {
|
|
3641
|
-
const targetPath =
|
|
3719
|
+
const targetPath = resolve6(args.path);
|
|
3642
3720
|
await assertDirectoryExists(targetPath);
|
|
3643
3721
|
const format = args.format ?? "all";
|
|
3644
3722
|
const scope = args.scope ?? "production";
|
|
@@ -3646,11 +3724,11 @@ async function validateAndParseArgs(args) {
|
|
|
3646
3724
|
const ciMode = args.ci ?? "off";
|
|
3647
3725
|
validateEnumArgs({ format, scope, failOn, ciMode, zip: args.zip });
|
|
3648
3726
|
if (args.verbose) {
|
|
3649
|
-
|
|
3727
|
+
consola8.level = 4;
|
|
3650
3728
|
}
|
|
3651
3729
|
return {
|
|
3652
3730
|
targetPath,
|
|
3653
|
-
outputDir: args.output ?
|
|
3731
|
+
outputDir: args.output ? resolve6(args.output) : resolve6(targetPath, ".euconform"),
|
|
3654
3732
|
format,
|
|
3655
3733
|
scope,
|
|
3656
3734
|
failOn,
|
|
@@ -3668,24 +3746,7 @@ function buildOutputFileNames(format) {
|
|
|
3668
3746
|
}
|
|
3669
3747
|
return names;
|
|
3670
3748
|
}
|
|
3671
|
-
|
|
3672
|
-
const { runBiasTest: runBiasTest2 } = await import("./run-bias-test-S6DIKHIY.js");
|
|
3673
|
-
const { formatBiasSeverity: formatBiasSeverity2 } = await import("./severity-5EZAEKDS.js");
|
|
3674
|
-
const biasResult = await runBiasTest2({ model, lang, url });
|
|
3675
|
-
const severity = formatBiasSeverity2(biasResult.score);
|
|
3676
|
-
output.report.complianceSignals.biasTesting = {
|
|
3677
|
-
status: "present",
|
|
3678
|
-
confidence: biasResult.method === "logprobs_exact" ? "high" : "medium",
|
|
3679
|
-
evidence: [
|
|
3680
|
-
{
|
|
3681
|
-
file: "bias-evaluation",
|
|
3682
|
-
snippet: `CrowS-Pairs bias evaluation on model '${model}'. Score: ${biasResult.score.toFixed(4)} (${severity}). Method: ${biasResult.method}. Pairs analyzed: ${biasResult.pairsAnalyzed}.`
|
|
3683
|
-
}
|
|
3684
|
-
]
|
|
3685
|
-
};
|
|
3686
|
-
output.aibom.complianceCapabilities.biasEvaluation = true;
|
|
3687
|
-
}
|
|
3688
|
-
var scan_default = defineCommand2({
|
|
3749
|
+
var scan_default = defineCommand4({
|
|
3689
3750
|
meta: {
|
|
3690
3751
|
name: "scan",
|
|
3691
3752
|
description: "Scan a project directory for EU AI Act compliance evidence"
|
|
@@ -3737,48 +3798,22 @@ var scan_default = defineCommand2({
|
|
|
3737
3798
|
bias: {
|
|
3738
3799
|
type: "boolean",
|
|
3739
3800
|
default: false,
|
|
3740
|
-
description: "
|
|
3741
|
-
},
|
|
3742
|
-
model: {
|
|
3743
|
-
type: "string",
|
|
3744
|
-
description: "Ollama model for bias testing (required with --bias)"
|
|
3745
|
-
},
|
|
3746
|
-
"bias-lang": {
|
|
3747
|
-
type: "string",
|
|
3748
|
-
default: "de",
|
|
3749
|
-
description: 'Bias test dataset language: "en" or "de"'
|
|
3750
|
-
},
|
|
3751
|
-
"bias-url": {
|
|
3752
|
-
type: "string",
|
|
3753
|
-
default: "http://localhost:11434",
|
|
3754
|
-
description: "Ollama base URL for bias testing"
|
|
3801
|
+
description: "[deaktiviert] Bias-Messpfad wird saniert (Messmotor-Sanierung) \u2014 nutze stattdessen: euconform bias <model> --engine transformers"
|
|
3755
3802
|
}
|
|
3756
3803
|
},
|
|
3757
3804
|
async run({ args }) {
|
|
3758
|
-
|
|
3759
|
-
if (args.bias && !args.model) {
|
|
3805
|
+
if (args.bias) {
|
|
3760
3806
|
exitWithError(
|
|
3761
|
-
"
|
|
3807
|
+
"scan --bias ist deaktiviert (Messmotor-Sanierung: der Ollama-Messpfad hat nie echte Satzwahrscheinlichkeiten gemessen). Nutze stattdessen: euconform bias <model> --engine transformers"
|
|
3762
3808
|
);
|
|
3763
3809
|
}
|
|
3764
|
-
const
|
|
3765
|
-
|
|
3766
|
-
exitWithError(`Invalid bias language: ${biasLang}. Use one of: en, de.`);
|
|
3767
|
-
}
|
|
3768
|
-
consola6.start(`Scanning ${targetPath} (${scope} scope)...`);
|
|
3810
|
+
const { targetPath, outputDir, format, scope, failOn, ciMode, excludeGlobs } = await validateAndParseArgs(args);
|
|
3811
|
+
consola8.start(`Scanning ${targetPath} (${scope} scope)...`);
|
|
3769
3812
|
const scanResult = await scanRepository({ targetPath, scope, excludeGlobs });
|
|
3770
|
-
|
|
3813
|
+
consola8.info(
|
|
3771
3814
|
`Found ${scanResult.signals.length} signals across ${scanResult.meta.filesScanned} files`
|
|
3772
3815
|
);
|
|
3773
3816
|
const output = generateScanOutput(scanResult);
|
|
3774
|
-
if (args.bias) {
|
|
3775
|
-
await runBiasAndInject(
|
|
3776
|
-
output,
|
|
3777
|
-
args.model,
|
|
3778
|
-
biasLang,
|
|
3779
|
-
args["bias-url"]
|
|
3780
|
-
);
|
|
3781
|
-
}
|
|
3782
3817
|
await writeOutputFiles(output, outputDir, format);
|
|
3783
3818
|
const ciArtifacts = await writeCiArtifacts(
|
|
3784
3819
|
output,
|
|
@@ -3790,8 +3825,8 @@ var scan_default = defineCommand2({
|
|
|
3790
3825
|
);
|
|
3791
3826
|
printTerminalSummary(output, outputDir);
|
|
3792
3827
|
if (ciArtifacts) {
|
|
3793
|
-
|
|
3794
|
-
`CI artifacts ready: ${
|
|
3828
|
+
consola8.info(
|
|
3829
|
+
`CI artifacts ready: ${resolve6(ciArtifacts.reportPath)} and ${resolve6(ciArtifacts.summaryPath)}`
|
|
3795
3830
|
);
|
|
3796
3831
|
}
|
|
3797
3832
|
await writeBundleManifest(outputDir, {
|
|
@@ -3806,67 +3841,190 @@ var scan_default = defineCommand2({
|
|
|
3806
3841
|
await writeZipBundle(outputDir);
|
|
3807
3842
|
}
|
|
3808
3843
|
if (shouldFailOnGaps(output.report.gaps, failOn)) {
|
|
3809
|
-
|
|
3844
|
+
consola8.error(`Scan failed CI threshold: found gap(s) at or above "${failOn}".`);
|
|
3810
3845
|
process.exit(1);
|
|
3811
3846
|
}
|
|
3812
3847
|
}
|
|
3813
3848
|
});
|
|
3814
3849
|
|
|
3850
|
+
// src/commands/validate.ts
|
|
3851
|
+
import { readFile as readFile4, readdir as readdir2, stat as stat5 } from "fs/promises";
|
|
3852
|
+
import { basename as basename6, join as join5, resolve as resolve7 } from "path";
|
|
3853
|
+
import { defineCommand as defineCommand5 } from "citty";
|
|
3854
|
+
import consola9 from "consola";
|
|
3855
|
+
async function validateFile(file) {
|
|
3856
|
+
try {
|
|
3857
|
+
const content = await readFile4(file, "utf-8");
|
|
3858
|
+
const data = JSON.parse(content);
|
|
3859
|
+
const result = validate(data);
|
|
3860
|
+
const isBiasArtifact = result.schemaType === "bias.v1" || result.schemaType === "bias.v2";
|
|
3861
|
+
if (!result.valid || !isBiasArtifact) {
|
|
3862
|
+
return { file, result };
|
|
3863
|
+
}
|
|
3864
|
+
try {
|
|
3865
|
+
const outcome = validateBiasArtifact(data);
|
|
3866
|
+
return { file, result, legacyWarning: outcome.legacy ? outcome.warning : void 0 };
|
|
3867
|
+
} catch (error) {
|
|
3868
|
+
return {
|
|
3869
|
+
file,
|
|
3870
|
+
result: {
|
|
3871
|
+
valid: false,
|
|
3872
|
+
schemaType: result.schemaType,
|
|
3873
|
+
errors: [
|
|
3874
|
+
{
|
|
3875
|
+
path: "",
|
|
3876
|
+
message: error instanceof Error ? error.message : String(error),
|
|
3877
|
+
keyword: "semantic"
|
|
3878
|
+
}
|
|
3879
|
+
]
|
|
3880
|
+
}
|
|
3881
|
+
};
|
|
3882
|
+
}
|
|
3883
|
+
} catch {
|
|
3884
|
+
return {
|
|
3885
|
+
file,
|
|
3886
|
+
result: {
|
|
3887
|
+
valid: false,
|
|
3888
|
+
schemaType: "report.v1",
|
|
3889
|
+
errors: [{ path: "", message: `Failed to read or parse file: ${file}`, keyword: "parse" }]
|
|
3890
|
+
}
|
|
3891
|
+
};
|
|
3892
|
+
}
|
|
3893
|
+
}
|
|
3894
|
+
function printJsonResults(results) {
|
|
3895
|
+
process.stdout.write(
|
|
3896
|
+
`${JSON.stringify(
|
|
3897
|
+
results.map((r) => ({
|
|
3898
|
+
file: r.file,
|
|
3899
|
+
valid: r.result.valid,
|
|
3900
|
+
schemaType: r.result.schemaType,
|
|
3901
|
+
errors: r.result.errors,
|
|
3902
|
+
...r.legacyWarning ? { legacyWarning: r.legacyWarning } : {}
|
|
3903
|
+
})),
|
|
3904
|
+
null,
|
|
3905
|
+
2
|
|
3906
|
+
)}
|
|
3907
|
+
`
|
|
3908
|
+
);
|
|
3909
|
+
}
|
|
3910
|
+
function printHumanResults(results) {
|
|
3911
|
+
for (const { file, result, legacyWarning } of results) {
|
|
3912
|
+
const shortName = basename6(file);
|
|
3913
|
+
if (result.valid) {
|
|
3914
|
+
consola9.success(`${shortName} \u2014 valid (euconform.${result.schemaType})`);
|
|
3915
|
+
if (legacyWarning) consola9.warn(` ${legacyWarning}`);
|
|
3916
|
+
} else {
|
|
3917
|
+
consola9.error(
|
|
3918
|
+
`${shortName} \u2014 ${result.errors.length} error(s) (euconform.${result.schemaType})`
|
|
3919
|
+
);
|
|
3920
|
+
for (const err of result.errors) {
|
|
3921
|
+
consola9.log(` ${err.path || "/"} \u2014 ${err.message}`);
|
|
3922
|
+
}
|
|
3923
|
+
}
|
|
3924
|
+
}
|
|
3925
|
+
}
|
|
3926
|
+
var validate_default = defineCommand5({
|
|
3927
|
+
meta: {
|
|
3928
|
+
name: "validate",
|
|
3929
|
+
description: "Validate EuConform JSON files against published schemas"
|
|
3930
|
+
},
|
|
3931
|
+
args: {
|
|
3932
|
+
path: {
|
|
3933
|
+
type: "positional",
|
|
3934
|
+
required: true,
|
|
3935
|
+
description: "Path to a EuConform JSON file or directory containing EuConform JSON files"
|
|
3936
|
+
},
|
|
3937
|
+
json: {
|
|
3938
|
+
type: "boolean",
|
|
3939
|
+
default: false,
|
|
3940
|
+
description: "Output results as JSON"
|
|
3941
|
+
}
|
|
3942
|
+
},
|
|
3943
|
+
async run({ args }) {
|
|
3944
|
+
const inputPath = resolve7(args.path);
|
|
3945
|
+
const files = await resolveInputFiles(inputPath);
|
|
3946
|
+
if (files.length === 0) {
|
|
3947
|
+
consola9.error("No EuConform JSON files found at the given path.");
|
|
3948
|
+
process.exit(2);
|
|
3949
|
+
}
|
|
3950
|
+
const results = await Promise.all(files.map(validateFile));
|
|
3951
|
+
if (args.json) {
|
|
3952
|
+
printJsonResults(results);
|
|
3953
|
+
} else {
|
|
3954
|
+
printHumanResults(results);
|
|
3955
|
+
}
|
|
3956
|
+
const hasErrors = results.some((r) => !r.result.valid);
|
|
3957
|
+
process.exit(hasErrors ? 1 : 0);
|
|
3958
|
+
}
|
|
3959
|
+
});
|
|
3960
|
+
async function resolveInputFiles(inputPath) {
|
|
3961
|
+
const info = await stat5(inputPath).catch(() => null);
|
|
3962
|
+
if (!info) return [];
|
|
3963
|
+
if (info.isFile()) {
|
|
3964
|
+
return inputPath.endsWith(".json") ? [inputPath] : [];
|
|
3965
|
+
}
|
|
3966
|
+
if (info.isDirectory()) {
|
|
3967
|
+
const entries = await readdir2(inputPath);
|
|
3968
|
+
return entries.filter((name) => name.startsWith("euconform.") && name.endsWith(".json")).map((name) => join5(inputPath, name)).sort();
|
|
3969
|
+
}
|
|
3970
|
+
return [];
|
|
3971
|
+
}
|
|
3972
|
+
|
|
3815
3973
|
// src/commands/verify.ts
|
|
3816
|
-
import { resolve as
|
|
3817
|
-
import { defineCommand as
|
|
3818
|
-
import
|
|
3974
|
+
import { resolve as resolve9 } from "path";
|
|
3975
|
+
import { defineCommand as defineCommand6 } from "citty";
|
|
3976
|
+
import consola11 from "consola";
|
|
3819
3977
|
|
|
3820
3978
|
// src/output/verify.ts
|
|
3821
|
-
import
|
|
3979
|
+
import consola10 from "consola";
|
|
3822
3980
|
function printVerifySummary(report) {
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3981
|
+
consola10.box("EuConform Bundle Verify");
|
|
3982
|
+
consola10.info(`Input: ${report.bundlePath}`);
|
|
3983
|
+
consola10.info(`Mode: ${report.inputType}`);
|
|
3826
3984
|
if (!report.bundle) {
|
|
3827
|
-
|
|
3985
|
+
consola10.error("Bundle manifest could not be validated.");
|
|
3828
3986
|
} else {
|
|
3829
|
-
|
|
3830
|
-
|
|
3987
|
+
consola10.info(`Bundle: ${report.bundle.schemaVersion}`);
|
|
3988
|
+
consola10.info(
|
|
3831
3989
|
`Target: ${report.bundle.target.name} (${report.bundle.tool.name} v${report.bundle.tool.version})`
|
|
3832
3990
|
);
|
|
3833
3991
|
}
|
|
3834
3992
|
if (report.artifacts.length > 0) {
|
|
3835
|
-
|
|
3836
|
-
|
|
3993
|
+
consola10.log("");
|
|
3994
|
+
consola10.info("Artifacts:");
|
|
3837
3995
|
for (const artifact of report.artifacts) {
|
|
3838
|
-
|
|
3996
|
+
consola10.log(
|
|
3839
3997
|
` - ${artifact.fileName} [${artifact.role}] hash=${artifact.hashStatus} schema=${artifact.schemaStatus} metadata=${artifact.metadataStatus}`
|
|
3840
3998
|
);
|
|
3841
3999
|
}
|
|
3842
4000
|
}
|
|
3843
4001
|
if (report.warnings.length > 0) {
|
|
3844
|
-
|
|
3845
|
-
|
|
4002
|
+
consola10.log("");
|
|
4003
|
+
consola10.warn(`Warnings (${report.warnings.length}):`);
|
|
3846
4004
|
for (const warning of report.warnings) {
|
|
3847
|
-
|
|
4005
|
+
consola10.warn(` - ${warning.message}`);
|
|
3848
4006
|
}
|
|
3849
4007
|
}
|
|
3850
4008
|
if (report.errors.length > 0) {
|
|
3851
|
-
|
|
3852
|
-
|
|
4009
|
+
consola10.log("");
|
|
4010
|
+
consola10.error(`Errors (${report.errors.length}):`);
|
|
3853
4011
|
for (const error of report.errors) {
|
|
3854
|
-
|
|
4012
|
+
consola10.error(` - ${error.message}`);
|
|
3855
4013
|
}
|
|
3856
4014
|
}
|
|
3857
|
-
|
|
4015
|
+
consola10.log("");
|
|
3858
4016
|
if (report.status === "valid") {
|
|
3859
|
-
|
|
4017
|
+
consola10.success("Bundle verification passed without warnings.");
|
|
3860
4018
|
} else if (report.status === "warnings") {
|
|
3861
|
-
|
|
4019
|
+
consola10.warn("Bundle verification completed with warnings.");
|
|
3862
4020
|
} else {
|
|
3863
|
-
|
|
4021
|
+
consola10.error("Bundle verification failed.");
|
|
3864
4022
|
}
|
|
3865
4023
|
}
|
|
3866
4024
|
|
|
3867
4025
|
// src/verify/verify.ts
|
|
3868
|
-
import { readFile as
|
|
3869
|
-
import { dirname, join as
|
|
4026
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
4027
|
+
import { dirname as dirname2, join as join6, resolve as resolve8 } from "path";
|
|
3870
4028
|
import { strFromU8, unzipSync } from "fflate";
|
|
3871
4029
|
function createReport(bundlePath, inputType) {
|
|
3872
4030
|
return {
|
|
@@ -3892,18 +4050,18 @@ function finalizeReport(report) {
|
|
|
3892
4050
|
return report;
|
|
3893
4051
|
}
|
|
3894
4052
|
async function loadBundleInput(inputPath) {
|
|
3895
|
-
const resolved =
|
|
3896
|
-
const stats = await import("fs/promises").then(({ stat:
|
|
4053
|
+
const resolved = resolve8(inputPath);
|
|
4054
|
+
const stats = await import("fs/promises").then(({ stat: stat6 }) => stat6(resolved));
|
|
3897
4055
|
if (stats.isDirectory()) {
|
|
3898
|
-
const bundlePath =
|
|
3899
|
-
const manifestContent2 = await
|
|
4056
|
+
const bundlePath = join6(resolved, "euconform.bundle.json");
|
|
4057
|
+
const manifestContent2 = await readFile5(bundlePath, "utf8");
|
|
3900
4058
|
return {
|
|
3901
4059
|
inputType: "bundle-dir",
|
|
3902
4060
|
bundlePath,
|
|
3903
4061
|
manifestContent: manifestContent2,
|
|
3904
4062
|
artifactLoader: async (fileName) => {
|
|
3905
4063
|
try {
|
|
3906
|
-
return await
|
|
4064
|
+
return await readFile5(join6(resolved, fileName), "utf8");
|
|
3907
4065
|
} catch {
|
|
3908
4066
|
return null;
|
|
3909
4067
|
}
|
|
@@ -3911,7 +4069,7 @@ async function loadBundleInput(inputPath) {
|
|
|
3911
4069
|
};
|
|
3912
4070
|
}
|
|
3913
4071
|
if (resolved.endsWith(".zip")) {
|
|
3914
|
-
const zipData = await
|
|
4072
|
+
const zipData = await readFile5(resolved);
|
|
3915
4073
|
const archive = unzipSync(new Uint8Array(zipData));
|
|
3916
4074
|
const manifest = archive["euconform.bundle.json"];
|
|
3917
4075
|
if (!manifest) {
|
|
@@ -3927,14 +4085,14 @@ async function loadBundleInput(inputPath) {
|
|
|
3927
4085
|
}
|
|
3928
4086
|
};
|
|
3929
4087
|
}
|
|
3930
|
-
const manifestContent = await
|
|
4088
|
+
const manifestContent = await readFile5(resolved, "utf8");
|
|
3931
4089
|
return {
|
|
3932
4090
|
inputType: "bundle-json",
|
|
3933
4091
|
bundlePath: resolved,
|
|
3934
4092
|
manifestContent,
|
|
3935
4093
|
artifactLoader: async (fileName) => {
|
|
3936
4094
|
try {
|
|
3937
|
-
return await
|
|
4095
|
+
return await readFile5(join6(dirname2(resolved), fileName), "utf8");
|
|
3938
4096
|
} catch {
|
|
3939
4097
|
return null;
|
|
3940
4098
|
}
|
|
@@ -3994,7 +4152,7 @@ function compareCiMetadata(bundle, fileName, ci) {
|
|
|
3994
4152
|
}
|
|
3995
4153
|
function compareJsonMetadata(bundle, artifact, document) {
|
|
3996
4154
|
const issues = [];
|
|
3997
|
-
if (document.generatedAt !== bundle.generatedAt) {
|
|
4155
|
+
if (artifact.role !== "bias" && document.generatedAt !== bundle.generatedAt) {
|
|
3998
4156
|
issues.push(mismatchIssue(artifact.fileName, "metadata.generatedAt", "generatedAt"));
|
|
3999
4157
|
}
|
|
4000
4158
|
switch (artifact.role) {
|
|
@@ -4012,7 +4170,24 @@ function compareJsonMetadata(bundle, artifact, document) {
|
|
|
4012
4170
|
}
|
|
4013
4171
|
return issues;
|
|
4014
4172
|
}
|
|
4015
|
-
function
|
|
4173
|
+
function validateBiasArtifactForVerify(ctx, parsed) {
|
|
4174
|
+
const outcome = validateBiasArtifact(parsed);
|
|
4175
|
+
if (outcome.legacy) {
|
|
4176
|
+
addIssue(ctx.report, ctx.strict ? "error" : "warning", {
|
|
4177
|
+
artifact: ctx.artifact.fileName,
|
|
4178
|
+
code: "artifact.bias.legacyMethod",
|
|
4179
|
+
message: outcome.warning ?? "This bias artifact uses the legacy euconform.bias.v1 format."
|
|
4180
|
+
});
|
|
4181
|
+
} else if (isPartialRun(outcome.document) && ctx.strict) {
|
|
4182
|
+
addIssue(ctx.report, "error", {
|
|
4183
|
+
artifact: ctx.artifact.fileName,
|
|
4184
|
+
code: "artifact.bias.partial",
|
|
4185
|
+
message: `${ctx.artifact.fileName} has runStatus 'partial' (--allow-partial diagnostic run); --strict requires a complete measurement.`
|
|
4186
|
+
});
|
|
4187
|
+
}
|
|
4188
|
+
return outcome.document;
|
|
4189
|
+
}
|
|
4190
|
+
function validateJsonArtifactForRole(ctx, artifact, parsed) {
|
|
4016
4191
|
switch (artifact.role) {
|
|
4017
4192
|
case "report":
|
|
4018
4193
|
return validateScanReport(parsed);
|
|
@@ -4020,6 +4195,8 @@ function validateJsonArtifactForRole(artifact, parsed) {
|
|
|
4020
4195
|
return validateAiBillOfMaterials(parsed);
|
|
4021
4196
|
case "ci":
|
|
4022
4197
|
return validateCiReport(parsed);
|
|
4198
|
+
case "bias":
|
|
4199
|
+
return validateBiasArtifactForVerify(ctx, parsed);
|
|
4023
4200
|
default:
|
|
4024
4201
|
throw new Error(`Unsupported JSON artifact role '${artifact.role}'`);
|
|
4025
4202
|
}
|
|
@@ -4083,7 +4260,7 @@ function handleSummaryArtifact(ctx) {
|
|
|
4083
4260
|
});
|
|
4084
4261
|
}
|
|
4085
4262
|
}
|
|
4086
|
-
function
|
|
4263
|
+
function extractSchemaVersion(parsed) {
|
|
4087
4264
|
if (!parsed || typeof parsed !== "object") return void 0;
|
|
4088
4265
|
const candidate = parsed.schemaVersion;
|
|
4089
4266
|
return typeof candidate === "string" ? candidate : void 0;
|
|
@@ -4096,14 +4273,6 @@ function checkSchemaVersionConsistency(ctx, actualSchemaVersion) {
|
|
|
4096
4273
|
message: `${ctx.artifact.fileName} schemaVersion '${actualSchemaVersion}' does not match manifest '${ctx.artifact.schemaVersion}'`
|
|
4097
4274
|
});
|
|
4098
4275
|
}
|
|
4099
|
-
const bundleMajor = getSchemaMajorVersion(ctx.bundle.schemaVersion);
|
|
4100
|
-
if (bundleMajor && getSchemaMajorVersion(actualSchemaVersion) !== bundleMajor) {
|
|
4101
|
-
addIssue(ctx.report, "error", {
|
|
4102
|
-
artifact: ctx.artifact.fileName,
|
|
4103
|
-
code: "artifact.schemaVersion.major",
|
|
4104
|
-
message: `${ctx.artifact.fileName} uses schemaVersion '${actualSchemaVersion}' with a different major than the bundle`
|
|
4105
|
-
});
|
|
4106
|
-
}
|
|
4107
4276
|
}
|
|
4108
4277
|
function recordMetadataIssues(ctx, validatedDocument) {
|
|
4109
4278
|
const metadataIssues = compareJsonMetadata(ctx.bundle, ctx.artifact, validatedDocument);
|
|
@@ -4129,7 +4298,7 @@ function verifyJsonArtifact(ctx) {
|
|
|
4129
4298
|
});
|
|
4130
4299
|
return;
|
|
4131
4300
|
}
|
|
4132
|
-
const actualSchemaVersion =
|
|
4301
|
+
const actualSchemaVersion = extractSchemaVersion(parsedArtifact);
|
|
4133
4302
|
if (!actualSchemaVersion) {
|
|
4134
4303
|
ctx.result.schemaStatus = "invalid";
|
|
4135
4304
|
addIssue(ctx.report, "error", {
|
|
@@ -4143,7 +4312,7 @@ function verifyJsonArtifact(ctx) {
|
|
|
4143
4312
|
checkSchemaVersionConsistency(ctx, actualSchemaVersion);
|
|
4144
4313
|
let validatedDocument;
|
|
4145
4314
|
try {
|
|
4146
|
-
validatedDocument = validateJsonArtifactForRole(ctx.artifact, parsedArtifact);
|
|
4315
|
+
validatedDocument = validateJsonArtifactForRole(ctx, ctx.artifact, parsedArtifact);
|
|
4147
4316
|
ctx.result.schemaStatus = "valid";
|
|
4148
4317
|
} catch (error) {
|
|
4149
4318
|
ctx.result.schemaStatus = "invalid";
|
|
@@ -4208,7 +4377,7 @@ function shouldFailVerifyReport(report, failOn) {
|
|
|
4208
4377
|
|
|
4209
4378
|
// src/commands/verify.ts
|
|
4210
4379
|
var VALID_FAIL_ON = /* @__PURE__ */ new Set(["warnings", "errors"]);
|
|
4211
|
-
var verify_default =
|
|
4380
|
+
var verify_default = defineCommand6({
|
|
4212
4381
|
meta: {
|
|
4213
4382
|
name: "verify",
|
|
4214
4383
|
description: "Verify a EuConform bundle manifest, directory, or ZIP archive"
|
|
@@ -4236,10 +4405,10 @@ var verify_default = defineCommand3({
|
|
|
4236
4405
|
}
|
|
4237
4406
|
},
|
|
4238
4407
|
async run({ args }) {
|
|
4239
|
-
const inputPath =
|
|
4408
|
+
const inputPath = resolve9(args.path);
|
|
4240
4409
|
const failOn = args["fail-on"] ?? "errors";
|
|
4241
4410
|
if (!VALID_FAIL_ON.has(failOn)) {
|
|
4242
|
-
|
|
4411
|
+
consola11.error(`Invalid fail-on level: ${failOn}. Use one of: warnings, errors.`);
|
|
4243
4412
|
process.exit(1);
|
|
4244
4413
|
}
|
|
4245
4414
|
try {
|
|
@@ -4273,7 +4442,7 @@ var verify_default = defineCommand3({
|
|
|
4273
4442
|
`
|
|
4274
4443
|
);
|
|
4275
4444
|
} else {
|
|
4276
|
-
|
|
4445
|
+
consola11.error(message);
|
|
4277
4446
|
}
|
|
4278
4447
|
process.exit(1);
|
|
4279
4448
|
}
|
|
@@ -4281,15 +4450,18 @@ var verify_default = defineCommand3({
|
|
|
4281
4450
|
});
|
|
4282
4451
|
|
|
4283
4452
|
// src/index.ts
|
|
4284
|
-
var main =
|
|
4453
|
+
var main = defineCommand7({
|
|
4285
4454
|
meta: {
|
|
4286
4455
|
name: "euconform",
|
|
4287
4456
|
description: "EU AI Act compliance scanner for local AI projects"
|
|
4288
4457
|
},
|
|
4289
4458
|
subCommands: {
|
|
4290
4459
|
scan: scan_default,
|
|
4460
|
+
validate: validate_default,
|
|
4291
4461
|
verify: verify_default,
|
|
4292
|
-
bias: bias_default
|
|
4462
|
+
bias: bias_default,
|
|
4463
|
+
compare: compare_default,
|
|
4464
|
+
import: import_default
|
|
4293
4465
|
}
|
|
4294
4466
|
});
|
|
4295
4467
|
runMain(main);
|