@euconform/cli 1.0.0 → 1.1.1

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/dist/index.js CHANGED
@@ -1,29 +1,16 @@
1
1
  #!/usr/bin/env node
2
- var __create = Object.create;
3
- var __defProp = Object.defineProperty;
4
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
- var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __commonJS = (cb, mod) => function __require() {
9
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
10
- };
11
- var __copyProps = (to, from, except, desc) => {
12
- if (from && typeof from === "object" || typeof from === "function") {
13
- for (let key of __getOwnPropNames(from))
14
- if (!__hasOwnProp.call(to, key) && key !== except)
15
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
- }
17
- return to;
18
- };
19
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
- // If the importer is in node compatibility mode or this is not an ESM
21
- // file that has been converted to a CommonJS file using a Babel-
22
- // compatible transform (i.e. "__esModule" has not been set), then set
23
- // "default" to the CommonJS "module.exports" for node compatibility.
24
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
- mod
26
- ));
2
+ import {
3
+ runBiasTest
4
+ } from "./chunk-F6VBKHY4.js";
5
+ import "./chunk-FEBJZMWY.js";
6
+ import "./chunk-JQTB43B2.js";
7
+ import {
8
+ formatBiasSeverity
9
+ } from "./chunk-AYXSWY3I.js";
10
+ import {
11
+ __commonJS,
12
+ __toESM
13
+ } from "./chunk-JSBRDJBE.js";
27
14
 
28
15
  // ../../node_modules/.pnpm/ignore@7.0.5/node_modules/ignore/index.js
29
16
  var require_ignore = __commonJS({
@@ -484,11 +471,137 @@ var require_ignore = __commonJS({
484
471
  });
485
472
 
486
473
  // src/index.ts
487
- import { defineCommand as defineCommand3, runMain } from "citty";
474
+ import { defineCommand as defineCommand4, runMain } from "citty";
475
+
476
+ // src/commands/bias.ts
477
+ import { mkdir, stat, writeFile } from "fs/promises";
478
+ import { resolve } from "path";
479
+ import { defineCommand } from "citty";
480
+ import consola2 from "consola";
481
+
482
+ // src/utils/exit.ts
483
+ import consola from "consola";
484
+ function exitWithError(message) {
485
+ consola.error(message);
486
+ process.exit(1);
487
+ }
488
+
489
+ // src/commands/bias.ts
490
+ var VALID_LANGS = /* @__PURE__ */ new Set(["en", "de"]);
491
+ var VALID_OUTPUTS = /* @__PURE__ */ new Set(["json", "md", "all"]);
492
+ var bias_default = defineCommand({
493
+ meta: {
494
+ name: "bias",
495
+ description: "Run CrowS-Pairs bias test on an Ollama model"
496
+ },
497
+ args: {
498
+ model: {
499
+ type: "positional",
500
+ description: "Ollama model name (e.g. llama3.2, mistral:7b)",
501
+ required: true
502
+ },
503
+ lang: {
504
+ type: "string",
505
+ description: "Dataset language: en or de",
506
+ default: "de"
507
+ },
508
+ url: {
509
+ type: "string",
510
+ description: "Ollama base URL",
511
+ default: "http://localhost:11434"
512
+ },
513
+ output: {
514
+ type: "string",
515
+ description: "Output format: json, md, or all",
516
+ default: "json"
517
+ },
518
+ "out-dir": {
519
+ type: "string",
520
+ description: "Output directory",
521
+ default: "./.euconform"
522
+ }
523
+ },
524
+ async run({ args }) {
525
+ const lang = args.lang;
526
+ const output = args.output;
527
+ const outDir = resolve(args["out-dir"]);
528
+ const model = args.model;
529
+ const url = args.url;
530
+ if (!VALID_LANGS.has(lang)) {
531
+ exitWithError(`Invalid language: ${lang}. Use one of: en, de.`);
532
+ }
533
+ if (!VALID_OUTPUTS.has(output)) {
534
+ exitWithError(`Invalid output format: ${output}. Use one of: json, md, all.`);
535
+ }
536
+ const result = await runBiasTest({
537
+ model,
538
+ lang,
539
+ url
540
+ });
541
+ try {
542
+ await stat(outDir);
543
+ } catch {
544
+ await mkdir(outDir, { recursive: true });
545
+ }
546
+ const biasOutput = {
547
+ schemaVersion: "euconform.bias.v1",
548
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
549
+ model,
550
+ dataset: `crows_pairs_${lang}`,
551
+ result: {
552
+ score: result.score,
553
+ method: result.method,
554
+ pairsAnalyzed: result.pairsAnalyzed,
555
+ stereotypicalPreference: result.stereotypicalPreference
556
+ }
557
+ };
558
+ const severity = formatBiasSeverity(result.score);
559
+ if (output === "json" || output === "all") {
560
+ const jsonPath = resolve(outDir, "euconform.bias.json");
561
+ await writeFile(jsonPath, JSON.stringify(biasOutput, null, 2));
562
+ consola2.success(`Written: ${jsonPath}`);
563
+ }
564
+ if (output === "md" || output === "all") {
565
+ const mdContent = [
566
+ "# EuConform Bias Test Report",
567
+ "",
568
+ `**Model:** ${model}`,
569
+ `**Dataset:** CrowS-Pairs (${lang.toUpperCase()})`,
570
+ `**Generated:** ${biasOutput.generatedAt}`,
571
+ "",
572
+ "## Results",
573
+ "",
574
+ "| Metric | Value |",
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}`);
598
+ }
599
+ }
600
+ });
488
601
 
489
602
  // src/commands/scan.ts
490
- import { stat as stat2 } from "fs/promises";
491
- import { resolve as resolve2 } from "path";
603
+ import { stat as stat3 } from "fs/promises";
604
+ import { resolve as resolve3 } from "path";
492
605
 
493
606
  // ../core/src/evidence/compliance-evaluation.ts
494
607
  var IMPLEMENTATION_PROVENANCE = /* @__PURE__ */ new Set(["runtime", "config"]);
@@ -1409,8 +1522,67 @@ function validateCiReport(data) {
1409
1522
  }
1410
1523
  var VALID_BUNDLE_ROLES = /* @__PURE__ */ new Set(["report", "aibom", "ci", "summary"]);
1411
1524
  var SHA256_HEX = /^[a-f0-9]{64}$/;
1412
- function validateScanBundle(data) {
1413
- const obj = assertObject(data, "bundle");
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`);
1552
+ }
1553
+ if (raw.mimeType !== void 0 && typeof raw.mimeType !== "string") {
1554
+ throw new Error(`Invalid bundle artifact: '${fileName}' has non-string mimeType`);
1555
+ }
1556
+ return {
1557
+ role,
1558
+ fileName,
1559
+ sha256,
1560
+ required,
1561
+ declaredSchemaVersion,
1562
+ mimeType: raw.mimeType
1563
+ };
1564
+ }
1565
+ function assertBundleArtifactIntegrity(fields, seenRoles, seenFiles, bundleMajor) {
1566
+ const { role, fileName, sha256, declaredSchemaVersion } = fields;
1567
+ if (!VALID_BUNDLE_ROLES.has(role)) {
1568
+ throw new Error(`Invalid bundle artifact: unsupported role '${role}'`);
1569
+ }
1570
+ if (!SHA256_HEX.test(sha256)) {
1571
+ throw new Error(`Invalid bundle artifact: '${fileName}' has invalid sha256`);
1572
+ }
1573
+ if (seenRoles.has(role)) {
1574
+ throw new Error(`Invalid bundle artifact: duplicate role '${role}'`);
1575
+ }
1576
+ if (seenFiles.has(fileName)) {
1577
+ throw new Error(`Invalid bundle artifact: duplicate fileName '${fileName}'`);
1578
+ }
1579
+ if (declaredSchemaVersion && bundleMajor && majorVersion(declaredSchemaVersion) !== bundleMajor) {
1580
+ throw new Error(
1581
+ `Invalid bundle artifact: '${fileName}' uses schemaVersion '${declaredSchemaVersion}' with a different major than the bundle`
1582
+ );
1583
+ }
1584
+ }
1585
+ function validateBundleHeader(obj) {
1414
1586
  requireSchemaVersion(obj, "euconform.bundle.v1", "bundle");
1415
1587
  requireField(obj, "generatedAt", "string", "bundle");
1416
1588
  const tool = requireField(obj, "tool", "object", "bundle");
@@ -1419,6 +1591,10 @@ function validateScanBundle(data) {
1419
1591
  const target = requireField(obj, "target", "object", "bundle");
1420
1592
  requireField(target, "name", "string", "bundle", "target.name");
1421
1593
  requireField(target, "rootPath", "string", "bundle", "target.rootPath");
1594
+ }
1595
+ function validateScanBundle(data) {
1596
+ const obj = assertObject(data, "bundle");
1597
+ validateBundleHeader(obj);
1422
1598
  const artifacts = requireField(obj, "artifacts", "array", "bundle");
1423
1599
  if (artifacts.length === 0) {
1424
1600
  throw new Error("Invalid bundle: 'artifacts' must contain at least one artifact");
@@ -1428,63 +1604,13 @@ function validateScanBundle(data) {
1428
1604
  const bundleMajor = majorVersion("euconform.bundle.v1");
1429
1605
  let hasRequiredReport = false;
1430
1606
  for (let index = 0; index < artifacts.length; index++) {
1431
- const artifact = assertObject(artifacts[index], "bundle artifact");
1432
- const role = requireField(
1433
- artifact,
1434
- "role",
1435
- "string",
1436
- "bundle artifact",
1437
- `artifacts[${index}].role`
1438
- );
1439
- const fileName = requireField(
1440
- artifact,
1441
- "fileName",
1442
- "string",
1443
- "bundle artifact",
1444
- `artifacts[${index}].fileName`
1445
- );
1446
- const sha256 = requireField(
1447
- artifact,
1448
- "sha256",
1449
- "string",
1450
- "bundle artifact",
1451
- `artifacts[${index}].sha256`
1452
- );
1453
- const required = requireField(
1454
- artifact,
1455
- "required",
1456
- "boolean",
1457
- "bundle artifact",
1458
- `artifacts[${index}].required`
1459
- );
1460
- if (!VALID_BUNDLE_ROLES.has(role)) {
1461
- throw new Error(`Invalid bundle artifact: unsupported role '${role}'`);
1462
- }
1463
- if (!SHA256_HEX.test(sha256)) {
1464
- throw new Error(`Invalid bundle artifact: '${fileName}' has invalid sha256`);
1465
- }
1466
- if (seenRoles.has(role)) {
1467
- throw new Error(`Invalid bundle artifact: duplicate role '${role}'`);
1468
- }
1469
- if (seenFiles.has(fileName)) {
1470
- throw new Error(`Invalid bundle artifact: duplicate fileName '${fileName}'`);
1471
- }
1472
- seenRoles.add(role);
1473
- seenFiles.add(fileName);
1474
- const declaredSchemaVersion = artifact.schemaVersion;
1475
- if (declaredSchemaVersion !== void 0 && typeof declaredSchemaVersion !== "string") {
1476
- throw new Error(`Invalid bundle artifact: '${fileName}' has non-string schemaVersion`);
1477
- }
1478
- if (artifact.mimeType !== void 0 && typeof artifact.mimeType !== "string") {
1479
- throw new Error(`Invalid bundle artifact: '${fileName}' has non-string mimeType`);
1480
- }
1481
- if (declaredSchemaVersion && bundleMajor && majorVersion(declaredSchemaVersion) !== bundleMajor) {
1482
- throw new Error(
1483
- `Invalid bundle artifact: '${fileName}' uses schemaVersion '${declaredSchemaVersion}' with a different major than the bundle`
1484
- );
1485
- }
1486
- if (role === "report") {
1487
- hasRequiredReport = required;
1607
+ const raw = assertObject(artifacts[index], "bundle artifact");
1608
+ const fields = readBundleArtifactFields(raw, index);
1609
+ assertBundleArtifactIntegrity(fields, seenRoles, seenFiles, bundleMajor);
1610
+ seenRoles.add(fields.role);
1611
+ seenFiles.add(fields.fileName);
1612
+ if (fields.role === "report") {
1613
+ hasRequiredReport = fields.required;
1488
1614
  }
1489
1615
  }
1490
1616
  if (!hasRequiredReport) {
@@ -2742,8 +2868,8 @@ function runAllDetectors(ctx) {
2742
2868
 
2743
2869
  // ../core/src/scanner/discovery.ts
2744
2870
  var import_ignore = __toESM(require_ignore(), 1);
2745
- import { readFile, readdir, stat } from "fs/promises";
2746
- import { basename as basename2, extname, join, resolve } from "path";
2871
+ import { readFile, readdir, stat as stat2 } from "fs/promises";
2872
+ import { basename as basename2, extname, join, resolve as resolve2 } from "path";
2747
2873
  var DEFAULT_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".json", ".md", ".yaml", ".yml"];
2748
2874
  var NAMED_FILE_PATTERNS = [
2749
2875
  /^package\.json$/,
@@ -2880,7 +3006,7 @@ function shouldProcessEntry(entry, targetPath, filterOpts) {
2880
3006
  }
2881
3007
  async function tryReadFile(absolutePath, entry, ext, provenance, maxSize) {
2882
3008
  try {
2883
- const fileStat = await stat(absolutePath);
3009
+ const fileStat = await stat2(absolutePath);
2884
3010
  if (!fileStat.isFile() || fileStat.size === 0) return "ignore";
2885
3011
  if (fileStat.size > maxSize) return "skip";
2886
3012
  const content = await readFile(absolutePath, "utf-8");
@@ -2898,7 +3024,7 @@ async function tryReadFile(absolutePath, entry, ext, provenance, maxSize) {
2898
3024
  }
2899
3025
  function resolveOptions(options) {
2900
3026
  return {
2901
- targetPath: resolve(options.targetPath),
3027
+ targetPath: resolve2(options.targetPath),
2902
3028
  scope: options.scope ?? DEFAULT_SCOPE,
2903
3029
  extensions: options.extensions ?? DEFAULT_EXTENSIONS,
2904
3030
  maxSize: options.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE,
@@ -3133,13 +3259,13 @@ async function scanRepository(options) {
3133
3259
  }
3134
3260
 
3135
3261
  // src/commands/scan.ts
3136
- import { defineCommand } from "citty";
3137
- import consola4 from "consola";
3262
+ import { defineCommand as defineCommand2 } from "citty";
3263
+ import consola6 from "consola";
3138
3264
 
3139
3265
  // src/output/ci.ts
3140
- import { appendFile, mkdir, writeFile } from "fs/promises";
3266
+ import { appendFile, mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
3141
3267
  import { join as join2 } from "path";
3142
- import consola from "consola";
3268
+ import consola3 from "consola";
3143
3269
 
3144
3270
  // src/utils/gap-priority.ts
3145
3271
  var GAP_PRIORITY_ORDER = {
@@ -3290,15 +3416,15 @@ function emitGitHubAnnotations(output) {
3290
3416
  }
3291
3417
  async function writeCiArtifacts(output, outputDir, failOn, mode, scanScope, baseArtifacts) {
3292
3418
  if (mode === "off") return null;
3293
- await mkdir(outputDir, { recursive: true });
3419
+ await mkdir2(outputDir, { recursive: true });
3294
3420
  const ciReport = buildCiReport(output, failOn, scanScope, baseArtifacts);
3295
3421
  const reportPath = join2(outputDir, "euconform.ci.json");
3296
3422
  const summaryPath = join2(outputDir, "euconform.ci-summary.md");
3297
- await writeFile(reportPath, `${JSON.stringify(ciReport, null, 2)}
3423
+ await writeFile2(reportPath, `${JSON.stringify(ciReport, null, 2)}
3298
3424
  `, "utf-8");
3299
- await writeFile(summaryPath, renderCiSummaryMarkdown(output, ciReport), "utf-8");
3300
- consola.success(`Written ${reportPath}`);
3301
- consola.success(`Written ${summaryPath}`);
3425
+ await writeFile2(summaryPath, renderCiSummaryMarkdown(output, ciReport), "utf-8");
3426
+ consola3.success(`Written ${reportPath}`);
3427
+ consola3.success(`Written ${summaryPath}`);
3302
3428
  let githubStepSummaryPath;
3303
3429
  if (mode === "github") {
3304
3430
  emitGitHubAnnotations(output);
@@ -3307,14 +3433,14 @@ async function writeCiArtifacts(output, outputDir, failOn, mode, scanScope, base
3307
3433
  await appendFile(stepSummaryPath, `
3308
3434
  ${renderCiSummaryMarkdown(output, ciReport)}`, "utf-8");
3309
3435
  githubStepSummaryPath = stepSummaryPath;
3310
- consola.info(`Appended GitHub step summary: ${stepSummaryPath}`);
3436
+ consola3.info(`Appended GitHub step summary: ${stepSummaryPath}`);
3311
3437
  }
3312
3438
  }
3313
3439
  return { reportPath, summaryPath, githubStepSummaryPath, baseArtifacts };
3314
3440
  }
3315
3441
 
3316
3442
  // src/output/terminal.ts
3317
- import consola2 from "consola";
3443
+ import consola4 from "consola";
3318
3444
  var AREA_LABELS = {
3319
3445
  disclosure: "Disclosure & Transparency",
3320
3446
  biasTesting: "Bias Testing",
@@ -3335,56 +3461,56 @@ function printSignalCounts(complianceSignals) {
3335
3461
  byConfidence[area.confidence]++;
3336
3462
  }
3337
3463
  const total = Object.values(complianceSignals).length;
3338
- consola2.info(
3464
+ consola4.info(
3339
3465
  `${total} compliance areas assessed (${byConfidence.high} high, ${byConfidence.medium} medium, ${byConfidence.low} low confidence)`
3340
3466
  );
3341
3467
  }
3342
3468
  function printComplianceOverview(complianceSignals) {
3343
- consola2.log("");
3344
- consola2.info("Compliance Overview:");
3469
+ consola4.log("");
3470
+ consola4.info("Compliance Overview:");
3345
3471
  for (const [key, group] of Object.entries(complianceSignals)) {
3346
3472
  const label = AREA_LABELS[key] ?? key;
3347
3473
  const icon = STATUS_ICONS[group.status] ?? "[??]";
3348
- consola2.log(` ${icon} ${label.padEnd(28)} ${group.status} (${group.confidence})`);
3474
+ consola4.log(` ${icon} ${label.padEnd(28)} ${group.status} (${group.confidence})`);
3349
3475
  }
3350
3476
  }
3351
3477
  function printTopGaps(gaps) {
3352
3478
  if (gaps.length === 0) return;
3353
- consola2.log("");
3354
- consola2.warn(`Top gaps (${gaps.length} total):`);
3479
+ consola4.log("");
3480
+ consola4.warn(`Top gaps (${gaps.length} total):`);
3355
3481
  for (const gap of gaps.slice(0, 3)) {
3356
- consola2.log(` - [${gap.priority}] ${gap.title}`);
3482
+ consola4.log(` - [${gap.priority}] ${gap.title}`);
3357
3483
  }
3358
3484
  if (gaps.length > 3) {
3359
- consola2.log(` ... and ${gaps.length - 3} more`);
3485
+ consola4.log(` ... and ${gaps.length - 3} more`);
3360
3486
  }
3361
3487
  }
3362
3488
  function printTerminalSummary(output, outputDir) {
3363
3489
  const { report } = output;
3364
- consola2.box("EuConform Scan Complete");
3490
+ consola4.box("EuConform Scan Complete");
3365
3491
  printSignalCounts(report.complianceSignals);
3366
3492
  const ai = report.aiFootprint;
3367
- consola2.info(
3493
+ consola4.info(
3368
3494
  `AI detected: ${ai.usesAI ? "yes" : "no"}${ai.inferenceModes.length > 0 ? ` | Inference: ${ai.inferenceModes.join(", ")}` : ""}`
3369
3495
  );
3370
3496
  printComplianceOverview(report.complianceSignals);
3371
3497
  printTopGaps(report.gaps);
3372
3498
  const openCount = report.assessmentHints.openQuestions.length;
3373
3499
  if (openCount > 0) {
3374
- consola2.info(`${openCount} open question${openCount > 1 ? "s" : ""} for manual review`);
3500
+ consola4.info(`${openCount} open question${openCount > 1 ? "s" : ""} for manual review`);
3375
3501
  }
3376
- consola2.log("");
3377
- consola2.success(`Output written to: ${outputDir}`);
3378
- consola2.info("Next steps:");
3379
- consola2.log(" - Review `euconform.report.json` and `euconform.summary.md`");
3380
- consola2.log(" - Complete role and risk classification in the EuConform web app");
3381
- consola2.log(" - Use `--ci github --fail-on <level>` to turn scans into CI checks");
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");
3382
3508
  }
3383
3509
 
3384
3510
  // src/output/writer.ts
3385
- import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
3511
+ import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
3386
3512
  import { join as join3 } from "path";
3387
- import consola3 from "consola";
3513
+ import consola5 from "consola";
3388
3514
  async function tryReadArtifact(outputDir, fileName) {
3389
3515
  try {
3390
3516
  const content = await readFile2(join3(outputDir, fileName), "utf-8");
@@ -3408,30 +3534,30 @@ async function writeBundleManifest(outputDir, metadata) {
3408
3534
  generatedAt: metadata.generatedAt
3409
3535
  });
3410
3536
  const bundlePath = join3(outputDir, "euconform.bundle.json");
3411
- await writeFile2(bundlePath, `${JSON.stringify(bundle, null, 2)}
3537
+ await writeFile3(bundlePath, `${JSON.stringify(bundle, null, 2)}
3412
3538
  `, "utf-8");
3413
- consola3.success(`Written ${bundlePath}`);
3539
+ consola5.success(`Written ${bundlePath}`);
3414
3540
  return bundlePath;
3415
3541
  }
3416
3542
  async function writeOutputFiles(output, outputDir, format) {
3417
- await mkdir2(outputDir, { recursive: true });
3543
+ await mkdir3(outputDir, { recursive: true });
3418
3544
  const written = [];
3419
3545
  if (format === "json" || format === "all") {
3420
3546
  const reportContent = JSON.stringify(output.report, null, 2);
3421
3547
  const reportPath = join3(outputDir, "euconform.report.json");
3422
- await writeFile2(reportPath, reportContent, "utf-8");
3423
- consola3.success(`Written ${reportPath}`);
3548
+ await writeFile3(reportPath, reportContent, "utf-8");
3549
+ consola5.success(`Written ${reportPath}`);
3424
3550
  written.push({ fileName: "euconform.report.json", content: reportContent });
3425
3551
  const aibomContent = JSON.stringify(output.aibom, null, 2);
3426
3552
  const aibomPath = join3(outputDir, "euconform.aibom.json");
3427
- await writeFile2(aibomPath, aibomContent, "utf-8");
3428
- consola3.success(`Written ${aibomPath}`);
3553
+ await writeFile3(aibomPath, aibomContent, "utf-8");
3554
+ consola5.success(`Written ${aibomPath}`);
3429
3555
  written.push({ fileName: "euconform.aibom.json", content: aibomContent });
3430
3556
  }
3431
3557
  if (format === "md" || format === "all") {
3432
3558
  const mdPath = join3(outputDir, "euconform.summary.md");
3433
- await writeFile2(mdPath, output.summaryMarkdown, "utf-8");
3434
- consola3.success(`Written ${mdPath}`);
3559
+ await writeFile3(mdPath, output.summaryMarkdown, "utf-8");
3560
+ consola5.success(`Written ${mdPath}`);
3435
3561
  written.push({ fileName: "euconform.summary.md", content: output.summaryMarkdown });
3436
3562
  }
3437
3563
  if (written.some((artifact) => artifact.fileName === "euconform.report.json")) {
@@ -3468,8 +3594,8 @@ async function writeZipBundle(outputDir) {
3468
3594
  }
3469
3595
  const zipped = zipSync(zipData);
3470
3596
  const zipPath = join3(outputDir, "euconform.bundle.zip");
3471
- await writeFile2(zipPath, zipped);
3472
- consola3.success(`Written ${zipPath}`);
3597
+ await writeFile3(zipPath, zipped);
3598
+ consola5.success(`Written ${zipPath}`);
3473
3599
  return zipPath;
3474
3600
  }
3475
3601
 
@@ -3478,55 +3604,58 @@ var VALID_FORMATS = /* @__PURE__ */ new Set(["json", "md", "all"]);
3478
3604
  var VALID_SCOPES = /* @__PURE__ */ new Set(["production", "all"]);
3479
3605
  var FAIL_ON_LEVELS = ["none", "critical", "high", "medium", "low"];
3480
3606
  var VALID_CI_MODES = /* @__PURE__ */ new Set(["off", "github"]);
3481
- async function validateAndParseArgs(args) {
3482
- const targetPath = resolve2(args.path);
3607
+ async function assertDirectoryExists(targetPath) {
3483
3608
  try {
3484
- const info = await stat2(targetPath);
3609
+ const info = await stat3(targetPath);
3485
3610
  if (!info.isDirectory()) {
3486
- consola4.error(`Not a directory: ${targetPath}`);
3487
- process.exit(1);
3611
+ exitWithError(`Not a directory: ${targetPath}`);
3488
3612
  }
3489
3613
  } catch {
3490
- consola4.error(`Path does not exist: ${targetPath}`);
3491
- process.exit(1);
3614
+ exitWithError(`Path does not exist: ${targetPath}`);
3492
3615
  }
3493
- const format = args.format ?? "all";
3494
- const scope = args.scope ?? "production";
3495
- const failOn = args["fail-on"] ?? "none";
3496
- const ciMode = args.ci ?? "off";
3497
- if (!VALID_FORMATS.has(format)) {
3498
- consola4.error(`Invalid format: ${format}. Use one of: json, md, all.`);
3499
- process.exit(1);
3616
+ }
3617
+ function validateEnumArgs(args) {
3618
+ if (!VALID_FORMATS.has(args.format)) {
3619
+ exitWithError(`Invalid format: ${args.format}. Use one of: json, md, all.`);
3500
3620
  }
3501
- if (!VALID_SCOPES.has(scope)) {
3502
- consola4.error(`Invalid scope: ${scope}. Use one of: production, all.`);
3503
- process.exit(1);
3621
+ if (!VALID_SCOPES.has(args.scope)) {
3622
+ exitWithError(`Invalid scope: ${args.scope}. Use one of: production, all.`);
3504
3623
  }
3505
- if (!FAIL_ON_LEVELS.includes(failOn)) {
3506
- consola4.error(`Invalid fail-on level: ${failOn}. Use one of: ${FAIL_ON_LEVELS.join(", ")}.`);
3507
- process.exit(1);
3624
+ if (!FAIL_ON_LEVELS.includes(args.failOn)) {
3625
+ exitWithError(
3626
+ `Invalid fail-on level: ${args.failOn}. Use one of: ${FAIL_ON_LEVELS.join(", ")}.`
3627
+ );
3508
3628
  }
3509
- if (!VALID_CI_MODES.has(ciMode)) {
3510
- consola4.error(`Invalid ci mode: ${ciMode}. Use one of: off, github.`);
3511
- process.exit(1);
3629
+ if (!VALID_CI_MODES.has(args.ciMode)) {
3630
+ exitWithError(`Invalid ci mode: ${args.ciMode}. Use one of: off, github.`);
3512
3631
  }
3513
- if (args.zip && format === "md") {
3514
- consola4.error('Cannot create euconform.bundle.zip when format is "md" only.');
3515
- process.exit(1);
3632
+ if (args.zip && args.format === "md") {
3633
+ exitWithError('Cannot create euconform.bundle.zip when format is "md" only.');
3516
3634
  }
3635
+ }
3636
+ function parseExcludeGlobs(raw) {
3637
+ if (!raw) return void 0;
3638
+ return Array.isArray(raw) ? raw : [raw];
3639
+ }
3640
+ async function validateAndParseArgs(args) {
3641
+ const targetPath = resolve3(args.path);
3642
+ await assertDirectoryExists(targetPath);
3643
+ const format = args.format ?? "all";
3644
+ const scope = args.scope ?? "production";
3645
+ const failOn = args["fail-on"] ?? "none";
3646
+ const ciMode = args.ci ?? "off";
3647
+ validateEnumArgs({ format, scope, failOn, ciMode, zip: args.zip });
3517
3648
  if (args.verbose) {
3518
- consola4.level = 4;
3649
+ consola6.level = 4;
3519
3650
  }
3520
- const rawExclude = args["exclude-glob"];
3521
- const excludeGlobs = rawExclude ? Array.isArray(rawExclude) ? rawExclude : [rawExclude] : void 0;
3522
3651
  return {
3523
3652
  targetPath,
3524
- outputDir: args.output ? resolve2(args.output) : resolve2(targetPath, ".euconform"),
3653
+ outputDir: args.output ? resolve3(args.output) : resolve3(targetPath, ".euconform"),
3525
3654
  format,
3526
3655
  scope,
3527
3656
  failOn,
3528
3657
  ciMode,
3529
- excludeGlobs
3658
+ excludeGlobs: parseExcludeGlobs(args["exclude-glob"])
3530
3659
  };
3531
3660
  }
3532
3661
  function buildOutputFileNames(format) {
@@ -3539,7 +3668,24 @@ function buildOutputFileNames(format) {
3539
3668
  }
3540
3669
  return names;
3541
3670
  }
3542
- var scan_default = defineCommand({
3671
+ async function runBiasAndInject(output, model, lang, url) {
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({
3543
3689
  meta: {
3544
3690
  name: "scan",
3545
3691
  description: "Scan a project directory for EU AI Act compliance evidence"
@@ -3587,16 +3733,52 @@ var scan_default = defineCommand({
3587
3733
  type: "boolean",
3588
3734
  default: false,
3589
3735
  description: "Enable verbose logging"
3736
+ },
3737
+ bias: {
3738
+ type: "boolean",
3739
+ default: false,
3740
+ description: "Run CrowS-Pairs bias test after scan (requires --model)"
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"
3590
3755
  }
3591
3756
  },
3592
3757
  async run({ args }) {
3593
3758
  const { targetPath, outputDir, format, scope, failOn, ciMode, excludeGlobs } = await validateAndParseArgs(args);
3594
- consola4.start(`Scanning ${targetPath} (${scope} scope)...`);
3759
+ if (args.bias && !args.model) {
3760
+ exitWithError(
3761
+ "Bias testing requires --model flag. Example: euconform scan ./project --bias --model llama3.2"
3762
+ );
3763
+ }
3764
+ const biasLang = args["bias-lang"] ?? "de";
3765
+ if (args.bias && !["en", "de"].includes(biasLang)) {
3766
+ exitWithError(`Invalid bias language: ${biasLang}. Use one of: en, de.`);
3767
+ }
3768
+ consola6.start(`Scanning ${targetPath} (${scope} scope)...`);
3595
3769
  const scanResult = await scanRepository({ targetPath, scope, excludeGlobs });
3596
- consola4.info(
3770
+ consola6.info(
3597
3771
  `Found ${scanResult.signals.length} signals across ${scanResult.meta.filesScanned} files`
3598
3772
  );
3599
3773
  const output = generateScanOutput(scanResult);
3774
+ if (args.bias) {
3775
+ await runBiasAndInject(
3776
+ output,
3777
+ args.model,
3778
+ biasLang,
3779
+ args["bias-url"]
3780
+ );
3781
+ }
3600
3782
  await writeOutputFiles(output, outputDir, format);
3601
3783
  const ciArtifacts = await writeCiArtifacts(
3602
3784
  output,
@@ -3608,8 +3790,8 @@ var scan_default = defineCommand({
3608
3790
  );
3609
3791
  printTerminalSummary(output, outputDir);
3610
3792
  if (ciArtifacts) {
3611
- consola4.info(
3612
- `CI artifacts ready: ${resolve2(ciArtifacts.reportPath)} and ${resolve2(ciArtifacts.summaryPath)}`
3793
+ consola6.info(
3794
+ `CI artifacts ready: ${resolve3(ciArtifacts.reportPath)} and ${resolve3(ciArtifacts.summaryPath)}`
3613
3795
  );
3614
3796
  }
3615
3797
  await writeBundleManifest(outputDir, {
@@ -3624,67 +3806,67 @@ var scan_default = defineCommand({
3624
3806
  await writeZipBundle(outputDir);
3625
3807
  }
3626
3808
  if (shouldFailOnGaps(output.report.gaps, failOn)) {
3627
- consola4.error(`Scan failed CI threshold: found gap(s) at or above "${failOn}".`);
3809
+ consola6.error(`Scan failed CI threshold: found gap(s) at or above "${failOn}".`);
3628
3810
  process.exit(1);
3629
3811
  }
3630
3812
  }
3631
3813
  });
3632
3814
 
3633
3815
  // src/commands/verify.ts
3634
- import { resolve as resolve4 } from "path";
3635
- import { defineCommand as defineCommand2 } from "citty";
3636
- import consola6 from "consola";
3816
+ import { resolve as resolve5 } from "path";
3817
+ import { defineCommand as defineCommand3 } from "citty";
3818
+ import consola8 from "consola";
3637
3819
 
3638
3820
  // src/output/verify.ts
3639
- import consola5 from "consola";
3821
+ import consola7 from "consola";
3640
3822
  function printVerifySummary(report) {
3641
- consola5.box("EuConform Bundle Verify");
3642
- consola5.info(`Input: ${report.bundlePath}`);
3643
- consola5.info(`Mode: ${report.inputType}`);
3823
+ consola7.box("EuConform Bundle Verify");
3824
+ consola7.info(`Input: ${report.bundlePath}`);
3825
+ consola7.info(`Mode: ${report.inputType}`);
3644
3826
  if (!report.bundle) {
3645
- consola5.error("Bundle manifest could not be validated.");
3827
+ consola7.error("Bundle manifest could not be validated.");
3646
3828
  } else {
3647
- consola5.info(`Bundle: ${report.bundle.schemaVersion}`);
3648
- consola5.info(
3829
+ consola7.info(`Bundle: ${report.bundle.schemaVersion}`);
3830
+ consola7.info(
3649
3831
  `Target: ${report.bundle.target.name} (${report.bundle.tool.name} v${report.bundle.tool.version})`
3650
3832
  );
3651
3833
  }
3652
3834
  if (report.artifacts.length > 0) {
3653
- consola5.log("");
3654
- consola5.info("Artifacts:");
3835
+ consola7.log("");
3836
+ consola7.info("Artifacts:");
3655
3837
  for (const artifact of report.artifacts) {
3656
- consola5.log(
3838
+ consola7.log(
3657
3839
  ` - ${artifact.fileName} [${artifact.role}] hash=${artifact.hashStatus} schema=${artifact.schemaStatus} metadata=${artifact.metadataStatus}`
3658
3840
  );
3659
3841
  }
3660
3842
  }
3661
3843
  if (report.warnings.length > 0) {
3662
- consola5.log("");
3663
- consola5.warn(`Warnings (${report.warnings.length}):`);
3844
+ consola7.log("");
3845
+ consola7.warn(`Warnings (${report.warnings.length}):`);
3664
3846
  for (const warning of report.warnings) {
3665
- consola5.warn(` - ${warning.message}`);
3847
+ consola7.warn(` - ${warning.message}`);
3666
3848
  }
3667
3849
  }
3668
3850
  if (report.errors.length > 0) {
3669
- consola5.log("");
3670
- consola5.error(`Errors (${report.errors.length}):`);
3851
+ consola7.log("");
3852
+ consola7.error(`Errors (${report.errors.length}):`);
3671
3853
  for (const error of report.errors) {
3672
- consola5.error(` - ${error.message}`);
3854
+ consola7.error(` - ${error.message}`);
3673
3855
  }
3674
3856
  }
3675
- consola5.log("");
3857
+ consola7.log("");
3676
3858
  if (report.status === "valid") {
3677
- consola5.success("Bundle verification passed without warnings.");
3859
+ consola7.success("Bundle verification passed without warnings.");
3678
3860
  } else if (report.status === "warnings") {
3679
- consola5.warn("Bundle verification completed with warnings.");
3861
+ consola7.warn("Bundle verification completed with warnings.");
3680
3862
  } else {
3681
- consola5.error("Bundle verification failed.");
3863
+ consola7.error("Bundle verification failed.");
3682
3864
  }
3683
3865
  }
3684
3866
 
3685
3867
  // src/verify/verify.ts
3686
3868
  import { readFile as readFile3 } from "fs/promises";
3687
- import { dirname, join as join4, resolve as resolve3 } from "path";
3869
+ import { dirname, join as join4, resolve as resolve4 } from "path";
3688
3870
  import { strFromU8, unzipSync } from "fflate";
3689
3871
  function createReport(bundlePath, inputType) {
3690
3872
  return {
@@ -3710,8 +3892,8 @@ function finalizeReport(report) {
3710
3892
  return report;
3711
3893
  }
3712
3894
  async function loadBundleInput(inputPath) {
3713
- const resolved = resolve3(inputPath);
3714
- const stats = await import("fs/promises").then(({ stat: stat3 }) => stat3(resolved));
3895
+ const resolved = resolve4(inputPath);
3896
+ const stats = await import("fs/promises").then(({ stat: stat4 }) => stat4(resolved));
3715
3897
  if (stats.isDirectory()) {
3716
3898
  const bundlePath = join4(resolved, "euconform.bundle.json");
3717
3899
  const manifestContent2 = await readFile3(bundlePath, "utf8");
@@ -3759,79 +3941,74 @@ async function loadBundleInput(inputPath) {
3759
3941
  }
3760
3942
  };
3761
3943
  }
3762
- function compareJsonMetadata(bundle, artifact, document) {
3944
+ function mismatchIssue(fileName, code, field) {
3945
+ return {
3946
+ artifact: fileName,
3947
+ code,
3948
+ message: `${fileName} ${field} does not match bundle manifest`
3949
+ };
3950
+ }
3951
+ function compareReportMetadata(bundle, fileName, report) {
3763
3952
  const issues = [];
3764
- if (document.generatedAt !== bundle.generatedAt) {
3953
+ if (report.tool.name !== bundle.tool.name) {
3954
+ issues.push(mismatchIssue(fileName, "metadata.tool.name", "tool.name"));
3955
+ }
3956
+ if (report.tool.version !== bundle.tool.version) {
3957
+ issues.push(mismatchIssue(fileName, "metadata.tool.version", "tool.version"));
3958
+ }
3959
+ if (report.target.name !== bundle.target.name) {
3960
+ issues.push(mismatchIssue(fileName, "metadata.target.name", "target.name"));
3961
+ }
3962
+ if (report.target.rootPath !== bundle.target.rootPath) {
3963
+ issues.push(mismatchIssue(fileName, "metadata.target.rootPath", "target.rootPath"));
3964
+ }
3965
+ return issues;
3966
+ }
3967
+ function compareAibomMetadata(bundle, fileName, aibom) {
3968
+ const issues = [];
3969
+ if (aibom.project.name !== bundle.target.name) {
3765
3970
  issues.push({
3766
- artifact: artifact.fileName,
3767
- code: "metadata.generatedAt",
3768
- message: `${artifact.fileName} generatedAt does not match bundle manifest`
3971
+ artifact: fileName,
3972
+ code: "metadata.project.name",
3973
+ message: `${fileName} project.name does not match bundle target.name`
3769
3974
  });
3770
3975
  }
3771
- if (artifact.role === "report") {
3772
- const report = document;
3773
- if (report.tool.name !== bundle.tool.name) {
3774
- issues.push({
3775
- artifact: artifact.fileName,
3776
- code: "metadata.tool.name",
3777
- message: `${artifact.fileName} tool.name does not match bundle manifest`
3778
- });
3779
- }
3780
- if (report.tool.version !== bundle.tool.version) {
3781
- issues.push({
3782
- artifact: artifact.fileName,
3783
- code: "metadata.tool.version",
3784
- message: `${artifact.fileName} tool.version does not match bundle manifest`
3785
- });
3786
- }
3787
- if (report.target.name !== bundle.target.name) {
3788
- issues.push({
3789
- artifact: artifact.fileName,
3790
- code: "metadata.target.name",
3791
- message: `${artifact.fileName} target.name does not match bundle manifest`
3792
- });
3793
- }
3794
- if (report.target.rootPath !== bundle.target.rootPath) {
3795
- issues.push({
3796
- artifact: artifact.fileName,
3797
- code: "metadata.target.rootPath",
3798
- message: `${artifact.fileName} target.rootPath does not match bundle manifest`
3799
- });
3800
- }
3976
+ if (aibom.project.rootPath !== bundle.target.rootPath) {
3977
+ issues.push({
3978
+ artifact: fileName,
3979
+ code: "metadata.project.rootPath",
3980
+ message: `${fileName} project.rootPath does not match bundle target.rootPath`
3981
+ });
3801
3982
  }
3802
- if (artifact.role === "aibom") {
3803
- const aibom = document;
3804
- if (aibom.project.name !== bundle.target.name) {
3805
- issues.push({
3806
- artifact: artifact.fileName,
3807
- code: "metadata.project.name",
3808
- message: `${artifact.fileName} project.name does not match bundle target.name`
3809
- });
3810
- }
3811
- if (aibom.project.rootPath !== bundle.target.rootPath) {
3812
- issues.push({
3813
- artifact: artifact.fileName,
3814
- code: "metadata.project.rootPath",
3815
- message: `${artifact.fileName} project.rootPath does not match bundle target.rootPath`
3816
- });
3817
- }
3983
+ return issues;
3984
+ }
3985
+ function compareCiMetadata(bundle, fileName, ci) {
3986
+ const issues = [];
3987
+ if (ci.target.name !== bundle.target.name) {
3988
+ issues.push(mismatchIssue(fileName, "metadata.target.name", "target.name"));
3818
3989
  }
3819
- if (artifact.role === "ci") {
3820
- const ci = document;
3821
- if (ci.target.name !== bundle.target.name) {
3822
- issues.push({
3823
- artifact: artifact.fileName,
3824
- code: "metadata.target.name",
3825
- message: `${artifact.fileName} target.name does not match bundle manifest`
3826
- });
3827
- }
3828
- if (ci.target.rootPath !== bundle.target.rootPath) {
3829
- issues.push({
3830
- artifact: artifact.fileName,
3831
- code: "metadata.target.rootPath",
3832
- message: `${artifact.fileName} target.rootPath does not match bundle manifest`
3833
- });
3834
- }
3990
+ if (ci.target.rootPath !== bundle.target.rootPath) {
3991
+ issues.push(mismatchIssue(fileName, "metadata.target.rootPath", "target.rootPath"));
3992
+ }
3993
+ return issues;
3994
+ }
3995
+ function compareJsonMetadata(bundle, artifact, document) {
3996
+ const issues = [];
3997
+ if (document.generatedAt !== bundle.generatedAt) {
3998
+ issues.push(mismatchIssue(artifact.fileName, "metadata.generatedAt", "generatedAt"));
3999
+ }
4000
+ switch (artifact.role) {
4001
+ case "report":
4002
+ issues.push(...compareReportMetadata(bundle, artifact.fileName, document));
4003
+ break;
4004
+ case "aibom":
4005
+ issues.push(
4006
+ ...compareAibomMetadata(bundle, artifact.fileName, document)
4007
+ );
4008
+ break;
4009
+ case "ci":
4010
+ issues.push(...compareCiMetadata(bundle, artifact.fileName, document));
4011
+ break;
3835
4012
  }
3836
4013
  return issues;
3837
4014
  }
@@ -3850,27 +4027,160 @@ function validateJsonArtifactForRole(artifact, parsed) {
3850
4027
  function metadataSeverity(strict) {
3851
4028
  return strict ? "error" : "warning";
3852
4029
  }
3853
- async function verifyBundleInput(inputPath, options = {}) {
3854
- const loaded = await loadBundleInput(inputPath);
3855
- const report = createReport(loaded.bundlePath, loaded.inputType);
4030
+ function parseBundleManifest(report, manifestContent) {
3856
4031
  let parsedManifest;
3857
4032
  try {
3858
- parsedManifest = JSON.parse(loaded.manifestContent);
4033
+ parsedManifest = JSON.parse(manifestContent);
3859
4034
  } catch {
3860
4035
  addIssue(report, "error", {
3861
4036
  code: "bundle.parse",
3862
4037
  message: "Failed to parse euconform.bundle.json as JSON"
3863
4038
  });
3864
- return finalizeReport(report);
4039
+ return null;
3865
4040
  }
3866
- let bundle;
3867
4041
  try {
3868
- bundle = validateScanBundle(parsedManifest);
4042
+ return validateScanBundle(parsedManifest);
3869
4043
  } catch (error) {
3870
4044
  addIssue(report, "error", {
3871
4045
  code: "bundle.invalid",
3872
4046
  message: error instanceof Error ? error.message : String(error)
3873
4047
  });
4048
+ return null;
4049
+ }
4050
+ }
4051
+ function createArtifactResult(artifact, overrides = {}) {
4052
+ return {
4053
+ role: artifact.role,
4054
+ fileName: artifact.fileName,
4055
+ required: artifact.required,
4056
+ schemaVersion: artifact.schemaVersion,
4057
+ hashStatus: "missing",
4058
+ schemaStatus: artifact.role === "summary" ? "skipped" : "invalid",
4059
+ metadataStatus: "skipped",
4060
+ ...overrides
4061
+ };
4062
+ }
4063
+ function checkHash(ctx) {
4064
+ if (sha256Hex(ctx.content) === ctx.artifact.sha256) {
4065
+ ctx.result.hashStatus = "match";
4066
+ } else {
4067
+ ctx.result.hashStatus = "mismatch";
4068
+ addIssue(ctx.report, metadataSeverity(ctx.strict), {
4069
+ artifact: ctx.artifact.fileName,
4070
+ code: "artifact.sha256",
4071
+ message: `${ctx.artifact.fileName} does not match the SHA-256 recorded in the bundle`
4072
+ });
4073
+ }
4074
+ }
4075
+ function handleSummaryArtifact(ctx) {
4076
+ ctx.result.schemaStatus = "skipped";
4077
+ ctx.result.metadataStatus = "skipped";
4078
+ if (ctx.artifact.mimeType && ctx.artifact.mimeType !== "text/markdown") {
4079
+ addIssue(ctx.report, metadataSeverity(ctx.strict), {
4080
+ artifact: ctx.artifact.fileName,
4081
+ code: "artifact.mimeType",
4082
+ message: `${ctx.artifact.fileName} has unexpected mimeType '${ctx.artifact.mimeType}'`
4083
+ });
4084
+ }
4085
+ }
4086
+ function extractSchemaVersion2(parsed) {
4087
+ if (!parsed || typeof parsed !== "object") return void 0;
4088
+ const candidate = parsed.schemaVersion;
4089
+ return typeof candidate === "string" ? candidate : void 0;
4090
+ }
4091
+ function checkSchemaVersionConsistency(ctx, actualSchemaVersion) {
4092
+ if (ctx.artifact.schemaVersion && ctx.artifact.schemaVersion !== actualSchemaVersion) {
4093
+ addIssue(ctx.report, "error", {
4094
+ artifact: ctx.artifact.fileName,
4095
+ code: "artifact.schemaVersion.mismatch",
4096
+ message: `${ctx.artifact.fileName} schemaVersion '${actualSchemaVersion}' does not match manifest '${ctx.artifact.schemaVersion}'`
4097
+ });
4098
+ }
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
+ }
4108
+ function recordMetadataIssues(ctx, validatedDocument) {
4109
+ const metadataIssues = compareJsonMetadata(ctx.bundle, ctx.artifact, validatedDocument);
4110
+ if (metadataIssues.length === 0) {
4111
+ ctx.result.metadataStatus = "match";
4112
+ return;
4113
+ }
4114
+ ctx.result.metadataStatus = "mismatch";
4115
+ for (const issue of metadataIssues) {
4116
+ addIssue(ctx.report, metadataSeverity(ctx.strict), issue);
4117
+ }
4118
+ }
4119
+ function verifyJsonArtifact(ctx) {
4120
+ let parsedArtifact;
4121
+ try {
4122
+ parsedArtifact = JSON.parse(ctx.content);
4123
+ } catch {
4124
+ ctx.result.schemaStatus = "invalid";
4125
+ addIssue(ctx.report, "error", {
4126
+ artifact: ctx.artifact.fileName,
4127
+ code: "artifact.parse",
4128
+ message: `${ctx.artifact.fileName} is not valid JSON`
4129
+ });
4130
+ return;
4131
+ }
4132
+ const actualSchemaVersion = extractSchemaVersion2(parsedArtifact);
4133
+ if (!actualSchemaVersion) {
4134
+ ctx.result.schemaStatus = "invalid";
4135
+ addIssue(ctx.report, "error", {
4136
+ artifact: ctx.artifact.fileName,
4137
+ code: "artifact.schemaVersion",
4138
+ message: `${ctx.artifact.fileName} is missing a string schemaVersion`
4139
+ });
4140
+ return;
4141
+ }
4142
+ ctx.result.schemaVersion = actualSchemaVersion;
4143
+ checkSchemaVersionConsistency(ctx, actualSchemaVersion);
4144
+ let validatedDocument;
4145
+ try {
4146
+ validatedDocument = validateJsonArtifactForRole(ctx.artifact, parsedArtifact);
4147
+ ctx.result.schemaStatus = "valid";
4148
+ } catch (error) {
4149
+ ctx.result.schemaStatus = "invalid";
4150
+ addIssue(ctx.report, "error", {
4151
+ artifact: ctx.artifact.fileName,
4152
+ code: "artifact.invalid",
4153
+ message: error instanceof Error ? error.message : String(error)
4154
+ });
4155
+ return;
4156
+ }
4157
+ recordMetadataIssues(ctx, validatedDocument);
4158
+ }
4159
+ async function verifyArtifact(artifact, bundle, loaded, report, strict) {
4160
+ const result = createArtifactResult(artifact);
4161
+ const content = await loaded.artifactLoader(artifact.fileName);
4162
+ if (content === null) {
4163
+ addIssue(report, "error", {
4164
+ artifact: artifact.fileName,
4165
+ code: "artifact.missing",
4166
+ message: `Referenced artifact '${artifact.fileName}' is missing`
4167
+ });
4168
+ return result;
4169
+ }
4170
+ const ctx = { artifact, bundle, content, report, result, strict };
4171
+ checkHash(ctx);
4172
+ if (artifact.role === "summary") {
4173
+ handleSummaryArtifact(ctx);
4174
+ return result;
4175
+ }
4176
+ verifyJsonArtifact(ctx);
4177
+ return result;
4178
+ }
4179
+ async function verifyBundleInput(inputPath, options = {}) {
4180
+ const loaded = await loadBundleInput(inputPath);
4181
+ const report = createReport(loaded.bundlePath, loaded.inputType);
4182
+ const bundle = parseBundleManifest(report, loaded.manifestContent);
4183
+ if (!bundle) {
3874
4184
  return finalizeReport(report);
3875
4185
  }
3876
4186
  report.bundle = {
@@ -3879,118 +4189,9 @@ async function verifyBundleInput(inputPath, options = {}) {
3879
4189
  tool: bundle.tool,
3880
4190
  target: bundle.target
3881
4191
  };
3882
- const bundleMajor = getSchemaMajorVersion(bundle.schemaVersion);
4192
+ const strict = Boolean(options.strict);
3883
4193
  for (const artifact of bundle.artifacts) {
3884
- const result = {
3885
- role: artifact.role,
3886
- fileName: artifact.fileName,
3887
- required: artifact.required,
3888
- schemaVersion: artifact.schemaVersion,
3889
- hashStatus: "missing",
3890
- schemaStatus: artifact.role === "summary" ? "skipped" : "invalid",
3891
- metadataStatus: "skipped"
3892
- };
3893
- const content = await loaded.artifactLoader(artifact.fileName);
3894
- if (content === null) {
3895
- result.hashStatus = "missing";
3896
- result.schemaStatus = artifact.role === "summary" ? "skipped" : "invalid";
3897
- result.metadataStatus = "skipped";
3898
- report.artifacts.push(result);
3899
- addIssue(report, "error", {
3900
- artifact: artifact.fileName,
3901
- code: "artifact.missing",
3902
- message: `Referenced artifact '${artifact.fileName}' is missing`
3903
- });
3904
- continue;
3905
- }
3906
- if (sha256Hex(content) === artifact.sha256) {
3907
- result.hashStatus = "match";
3908
- } else {
3909
- result.hashStatus = "mismatch";
3910
- addIssue(report, metadataSeverity(Boolean(options.strict)), {
3911
- artifact: artifact.fileName,
3912
- code: "artifact.sha256",
3913
- message: `${artifact.fileName} does not match the SHA-256 recorded in the bundle`
3914
- });
3915
- }
3916
- if (artifact.role === "summary") {
3917
- result.schemaStatus = "skipped";
3918
- result.metadataStatus = "skipped";
3919
- if (artifact.mimeType && artifact.mimeType !== "text/markdown") {
3920
- addIssue(report, metadataSeverity(Boolean(options.strict)), {
3921
- artifact: artifact.fileName,
3922
- code: "artifact.mimeType",
3923
- message: `${artifact.fileName} has unexpected mimeType '${artifact.mimeType}'`
3924
- });
3925
- }
3926
- report.artifacts.push(result);
3927
- continue;
3928
- }
3929
- let parsedArtifact;
3930
- try {
3931
- parsedArtifact = JSON.parse(content);
3932
- } catch {
3933
- result.schemaStatus = "invalid";
3934
- result.metadataStatus = "skipped";
3935
- report.artifacts.push(result);
3936
- addIssue(report, "error", {
3937
- artifact: artifact.fileName,
3938
- code: "artifact.parse",
3939
- message: `${artifact.fileName} is not valid JSON`
3940
- });
3941
- continue;
3942
- }
3943
- const actualSchemaVersion = parsedArtifact && typeof parsedArtifact === "object" ? parsedArtifact.schemaVersion : void 0;
3944
- if (typeof actualSchemaVersion !== "string") {
3945
- result.schemaStatus = "invalid";
3946
- result.metadataStatus = "skipped";
3947
- report.artifacts.push(result);
3948
- addIssue(report, "error", {
3949
- artifact: artifact.fileName,
3950
- code: "artifact.schemaVersion",
3951
- message: `${artifact.fileName} is missing a string schemaVersion`
3952
- });
3953
- continue;
3954
- }
3955
- result.schemaVersion = actualSchemaVersion;
3956
- if (artifact.schemaVersion && artifact.schemaVersion !== actualSchemaVersion) {
3957
- addIssue(report, "error", {
3958
- artifact: artifact.fileName,
3959
- code: "artifact.schemaVersion.mismatch",
3960
- message: `${artifact.fileName} schemaVersion '${actualSchemaVersion}' does not match manifest '${artifact.schemaVersion}'`
3961
- });
3962
- }
3963
- if (bundleMajor && getSchemaMajorVersion(actualSchemaVersion) !== bundleMajor) {
3964
- addIssue(report, "error", {
3965
- artifact: artifact.fileName,
3966
- code: "artifact.schemaVersion.major",
3967
- message: `${artifact.fileName} uses schemaVersion '${actualSchemaVersion}' with a different major than the bundle`
3968
- });
3969
- }
3970
- let validatedDocument;
3971
- try {
3972
- validatedDocument = validateJsonArtifactForRole(artifact, parsedArtifact);
3973
- result.schemaStatus = "valid";
3974
- } catch (error) {
3975
- result.schemaStatus = "invalid";
3976
- result.metadataStatus = "skipped";
3977
- report.artifacts.push(result);
3978
- addIssue(report, "error", {
3979
- artifact: artifact.fileName,
3980
- code: "artifact.invalid",
3981
- message: error instanceof Error ? error.message : String(error)
3982
- });
3983
- continue;
3984
- }
3985
- const metadataIssues = compareJsonMetadata(bundle, artifact, validatedDocument);
3986
- if (metadataIssues.length === 0) {
3987
- result.metadataStatus = "match";
3988
- } else {
3989
- result.metadataStatus = "mismatch";
3990
- for (const issue of metadataIssues) {
3991
- addIssue(report, metadataSeverity(Boolean(options.strict)), issue);
3992
- }
3993
- }
4194
+ const result = await verifyArtifact(artifact, bundle, loaded, report, strict);
3994
4195
  report.artifacts.push(result);
3995
4196
  }
3996
4197
  return finalizeReport(report);
@@ -4007,7 +4208,7 @@ function shouldFailVerifyReport(report, failOn) {
4007
4208
 
4008
4209
  // src/commands/verify.ts
4009
4210
  var VALID_FAIL_ON = /* @__PURE__ */ new Set(["warnings", "errors"]);
4010
- var verify_default = defineCommand2({
4211
+ var verify_default = defineCommand3({
4011
4212
  meta: {
4012
4213
  name: "verify",
4013
4214
  description: "Verify a EuConform bundle manifest, directory, or ZIP archive"
@@ -4035,10 +4236,10 @@ var verify_default = defineCommand2({
4035
4236
  }
4036
4237
  },
4037
4238
  async run({ args }) {
4038
- const inputPath = resolve4(args.path);
4239
+ const inputPath = resolve5(args.path);
4039
4240
  const failOn = args["fail-on"] ?? "errors";
4040
4241
  if (!VALID_FAIL_ON.has(failOn)) {
4041
- consola6.error(`Invalid fail-on level: ${failOn}. Use one of: warnings, errors.`);
4242
+ consola8.error(`Invalid fail-on level: ${failOn}. Use one of: warnings, errors.`);
4042
4243
  process.exit(1);
4043
4244
  }
4044
4245
  try {
@@ -4072,7 +4273,7 @@ var verify_default = defineCommand2({
4072
4273
  `
4073
4274
  );
4074
4275
  } else {
4075
- consola6.error(message);
4276
+ consola8.error(message);
4076
4277
  }
4077
4278
  process.exit(1);
4078
4279
  }
@@ -4080,14 +4281,15 @@ var verify_default = defineCommand2({
4080
4281
  });
4081
4282
 
4082
4283
  // src/index.ts
4083
- var main = defineCommand3({
4284
+ var main = defineCommand4({
4084
4285
  meta: {
4085
4286
  name: "euconform",
4086
4287
  description: "EU AI Act compliance scanner for local AI projects"
4087
4288
  },
4088
4289
  subCommands: {
4089
4290
  scan: scan_default,
4090
- verify: verify_default
4291
+ verify: verify_default,
4292
+ bias: bias_default
4091
4293
  }
4092
4294
  });
4093
4295
  runMain(main);