@cejel/cejel 0.4.5 → 0.4.6

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.
@@ -5,19 +5,21 @@ import {
5
5
  describeGitFailure,
6
6
  execGit,
7
7
  isExpectedGitAbsence
8
- } from "./chunk-EPK7UTIK.js";
8
+ } from "./chunk-BZXBLDCJ.js";
9
9
  import {
10
10
  external_exports
11
- } from "./chunk-KGFGX3TP.js";
11
+ } from "./chunk-Y6NO5HAI.js";
12
12
  import {
13
+ MAX_REPOSITORY_CONTENT_BYTES,
13
14
  init_esm_shims,
14
15
  readRepoText,
15
16
  readRepoTextPrefix,
16
17
  recordContentSkip,
18
+ recordContentSkipForCriteria,
17
19
  recordFilesystemSkip,
18
20
  trackContentReads,
19
21
  withContentReadCriterion
20
- } from "./chunk-R4DPQLS2.js";
22
+ } from "./chunk-H55KECVT.js";
21
23
 
22
24
  // src/witan/schemas.ts
23
25
  init_esm_shims();
@@ -117,10 +119,26 @@ var WitanEvidencePointerSchema = external_exports.object({
117
119
  }).strict().refine((value) => Boolean(value.url ?? value.path ?? value.contentHash), {
118
120
  message: "Witan evidence must include a path, url, or content hash."
119
121
  });
122
+ var WitanInventoryScanDerivationSchema = external_exports.object({
123
+ kind: external_exports.literal("inventory-scan"),
124
+ inventoryScope: external_exports.enum(["tracked-scan-eligible", "tracked-complete"]),
125
+ inventoryCount: external_exports.number().int().nonnegative(),
126
+ patternSetId: external_exports.string().min(1).max(180),
127
+ patternCount: external_exports.number().int().positive(),
128
+ matchCount: external_exports.number().int().nonnegative()
129
+ }).strict();
130
+ var WitanFindingDerivationSchema = external_exports.discriminatedUnion("kind", [
131
+ WitanInventoryScanDerivationSchema
132
+ ]);
120
133
  var WitanFindingSchema = external_exports.object({
121
134
  severity: WitanFindingSeveritySchema,
122
135
  summary: external_exports.string().min(1).max(500),
123
- evidence: WitanEvidencePointerSchema
136
+ evidence: WitanEvidencePointerSchema,
137
+ // Absence findings emitted by the native repository detector must name the exact
138
+ // inventory and versioned pattern set whose zero result supports the proposition.
139
+ // This remains optional at the shared schema boundary because positive findings and
140
+ // semantic derivations are separate contracts; native absence emitters require it.
141
+ derivation: WitanFindingDerivationSchema.optional()
124
142
  }).strict();
125
143
  var WitanCriterionMetricSchema = external_exports.object({
126
144
  name: external_exports.string().min(1).max(80),
@@ -321,6 +339,7 @@ var WitanReportSchema = external_exports.preprocess((value) => {
321
339
  }, StrictWitanReportSchema);
322
340
  var WITAN_ATTESTATION_STATEMENT_TYPE = "https://in-toto.io/Statement/v1";
323
341
  var WITAN_ATTESTATION_PREDICATE_TYPE = "https://cejel.dev/attestations/scan/v1";
342
+ var WITAN_REPORT_FORMAT_VERSION = "1.0";
324
343
  var WitanAttestationOutcomeSchema = external_exports.discriminatedUnion("status", [
325
344
  external_exports.object({
326
345
  status: external_exports.literal("scored"),
@@ -350,6 +369,10 @@ var WitanAttestationStatementSchema = external_exports.object({
350
369
  version: external_exports.string().min(1).max(80)
351
370
  }).strict(),
352
371
  generatedAt: external_exports.string().datetime({ offset: true }),
372
+ // Optional on read for attestations emitted before Evidence Seam v1. New attestations
373
+ // always carry it, keeping report.json byte-stable while giving paired consumers an
374
+ // explicit report contract version.
375
+ reportFormatVersion: external_exports.string().regex(/^1\.\d+$/).optional(),
353
376
  rubricVersion: external_exports.string().min(1).max(120),
354
377
  repository: external_exports.object({
355
378
  productSlug: external_exports.string().regex(/^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$/),
@@ -421,7 +444,9 @@ init_esm_shims();
421
444
  // src/witan/abstention.ts
422
445
  init_esm_shims();
423
446
  var WITAN_NO_MEASUREMENT_REASON = "No free-core rubric criterion produced a measurable signal. Cejel abstains rather than publish a numeric zero for an entirely unmeasured repository.";
424
- var WITAN_AUTHENTICATED_A1_ABSENCE_SUMMARY = "Reviewable source is present, but the complete tracked inventory contains no concrete test file, configured test runner, CI test command, or coverage configuration.";
447
+ var WITAN_AUTHENTICATED_A1_ABSENCE_SUMMARY = "Reviewable source is present, but the tracked scan-eligible inventory matched no concrete test file, configured test runner, CI test command, or coverage configuration.";
448
+ var WITAN_DIRECTORY_WALK_AUTHENTICATED_A1_ABSENCE_SUMMARY = "Reviewable source is present, but the bounded directory walk matched no concrete test file, configured test runner, CI test command, or coverage configuration.";
449
+ var WITAN_LEGACY_AUTHENTICATED_A1_ABSENCE_SUMMARY = "Reviewable source is present, but the complete tracked inventory contains no concrete test file, configured test runner, CI test command, or coverage configuration.";
425
450
  function isWitanNoMeasurementAbstention(report) {
426
451
  return report.verdict === "insufficient_source" && report.insufficientSourceReason.startsWith(WITAN_NO_MEASUREMENT_REASON) && report.criteria.every(
427
452
  ({ status }) => status === "not_applicable" || status === "insufficient_data"
@@ -1027,6 +1052,7 @@ function formatMetricMember(criterion, metric2) {
1027
1052
  }
1028
1053
 
1029
1054
  // src/witan/html.ts
1055
+ var CEJEL_CERTIFICATE_FORMAT_VERSION = "1.0";
1030
1056
  function renderWitanHtmlReport(report, options = {}) {
1031
1057
  const codeCriteria = criteriaByCategory(report, "code_trust");
1032
1058
  const processCriteria = criteriaByCategory(report, "process_trust");
@@ -1045,6 +1071,7 @@ function renderWitanHtmlReport(report, options = {}) {
1045
1071
  <head>
1046
1072
  <meta charset="utf-8">
1047
1073
  <meta name="viewport" content="width=device-width, initial-scale=1">
1074
+ <meta name="cejel-certificate-format" content="${CEJEL_CERTIFICATE_FORMAT_VERSION}">
1048
1075
  <title>Cejel Trust Certificate - ${escapeHtml(report.productDisplayName)}</title>
1049
1076
  <style>${CERTIFICATE_CSS}</style>
1050
1077
  </head>
@@ -1639,6 +1666,7 @@ function createWitanAttestation(report, options) {
1639
1666
  predicate: {
1640
1667
  tool: { name: "cejel", version: options.toolVersion },
1641
1668
  generatedAt: options.generatedAt,
1669
+ reportFormatVersion: WITAN_REPORT_FORMAT_VERSION,
1642
1670
  rubricVersion: report.rubricVersion,
1643
1671
  repository: {
1644
1672
  productSlug: report.productSlug,
@@ -1724,6 +1752,23 @@ init_esm_shims();
1724
1752
  // src/witan/markdown.ts
1725
1753
  init_esm_shims();
1726
1754
 
1755
+ // src/presentation-safety.ts
1756
+ init_esm_shims();
1757
+ var PRESENTATION_CONTROL_PATTERN = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu;
1758
+ function sanitizePresentationLine(value, options) {
1759
+ const withoutControls = value.replace(PRESENTATION_CONTROL_PATTERN, "").trim();
1760
+ const safeValue = withoutControls.length > 0 ? withoutControls : options.fallback;
1761
+ if (safeValue.length <= options.maxLength) return safeValue;
1762
+ const ellipsis = ".".repeat(Math.min(3, options.maxLength));
1763
+ const contentLimit = Math.max(options.maxLength - ellipsis.length, 0);
1764
+ let prefix = "";
1765
+ for (const character of safeValue) {
1766
+ if (prefix.length + character.length > contentLimit) break;
1767
+ prefix += character;
1768
+ }
1769
+ return `${prefix}${ellipsis}`;
1770
+ }
1771
+
1727
1772
  // src/witan/repo-signals.ts
1728
1773
  init_esm_shims();
1729
1774
  import { createHash as createHash2 } from "crypto";
@@ -1745,6 +1790,71 @@ function stripBom(text) {
1745
1790
  function usesV17DetectorClosure(rubricVersion) {
1746
1791
  return rubricVersion === WITAN_RUBRIC_VERSION_V17 || rubricVersion === WITAN_RUBRIC_VERSION_V18 || rubricVersion === WITAN_RUBRIC_VERSION_V19 || rubricVersion === WITAN_RUBRIC_VERSION_V20 || rubricVersion === WITAN_RUBRIC_VERSION_V21 || rubricVersion === WITAN_RUBRIC_VERSION_V22;
1747
1792
  }
1793
+ var LEGACY_INVENTORY_ABSENCE_CONTEXT = {
1794
+ rubricGeneration: "legacy",
1795
+ inventoryProvenance: "directory-walk"
1796
+ };
1797
+ var INVENTORY_SCAN_PATTERN_SETS = {
1798
+ a1AuthenticatedAbsence: {
1799
+ id: "cejel.core-a1.test-integrity-surface.v1",
1800
+ count: 4
1801
+ },
1802
+ a1ConcreteTests: {
1803
+ id: "cejel.core-a1.concrete-test-files.v1",
1804
+ count: 16
1805
+ },
1806
+ a1CoverageConfiguration: {
1807
+ id: "cejel.core-a1.coverage-configuration.v1",
1808
+ count: 10
1809
+ },
1810
+ a2CurrentSecret: {
1811
+ id: "cejel.core-a2.current-secret-shape.v1",
1812
+ count: 3
1813
+ },
1814
+ a2RlsPolicy: {
1815
+ id: "cejel.core-a2.rls-policy.v1",
1816
+ count: 3
1817
+ },
1818
+ a3CiOrReleaseDeploy: {
1819
+ id: "cejel.core-a3.ci-or-release-deploy.v1",
1820
+ count: 12
1821
+ },
1822
+ a4Lockfile: {
1823
+ id: "cejel.core-a4.lockfile.v1",
1824
+ count: 11
1825
+ },
1826
+ a5ClaimRealityArtifact: {
1827
+ id: "cejel.core-a5.claim-reality-artifact.v1",
1828
+ count: 3
1829
+ }
1830
+ };
1831
+ function inventoryScanDerivation(inventoryCount, patternSet, matchCount) {
1832
+ return {
1833
+ kind: "inventory-scan",
1834
+ inventoryScope: "tracked-scan-eligible",
1835
+ inventoryCount,
1836
+ patternSetId: patternSet.id,
1837
+ patternCount: patternSet.count,
1838
+ matchCount
1839
+ };
1840
+ }
1841
+ function inventoryAbsenceFinding(options) {
1842
+ const { context, summaries } = options;
1843
+ const summary = context.rubricGeneration === "legacy" ? summaries.legacy : context.inventoryProvenance === "tracked-scan-eligible" ? summaries.currentTracked : summaries.currentDirectoryWalk;
1844
+ const emitsDerivation = context.rubricGeneration === "current" && context.inventoryProvenance === "tracked-scan-eligible";
1845
+ return {
1846
+ severity: options.severity,
1847
+ summary,
1848
+ evidence: options.evidence,
1849
+ ...emitsDerivation ? {
1850
+ derivation: inventoryScanDerivation(
1851
+ options.inventoryCount,
1852
+ options.patternSet,
1853
+ options.matchCount
1854
+ )
1855
+ } : {}
1856
+ };
1857
+ }
1748
1858
  function buildWitanInputFromRepo(options) {
1749
1859
  const tracked = trackContentReads(options.repoPath, () => buildWitanInputFromRepoUntracked(options));
1750
1860
  const affectedCriteria = tracked.affectedCriteria;
@@ -1783,6 +1893,10 @@ function buildWitanInputFromRepoUntracked(options) {
1783
1893
  const inventoryFiles = fileInventory.inventoryFiles;
1784
1894
  const ignoredTargetReason = repoFiles.length === 0 ? explainIgnoredScanTarget(options.repoPath) : void 0;
1785
1895
  const usesV17Detectors = usesV17DetectorClosure(rubricVersion);
1896
+ const inventoryAbsenceContext = {
1897
+ rubricGeneration: usesV17Detectors ? "current" : "legacy",
1898
+ inventoryProvenance: fileInventory.repoFileInventoryScope === "tracked-scan-eligible" ? "tracked-scan-eligible" : "directory-walk"
1899
+ };
1786
1900
  const usesV18NativeRls = rubricVersion === WITAN_RUBRIC_VERSION_V18 || rubricVersion === WITAN_RUBRIC_VERSION_V19 || rubricVersion === WITAN_RUBRIC_VERSION_V20 || rubricVersion === WITAN_RUBRIC_VERSION_V21 || rubricVersion === WITAN_RUBRIC_VERSION_V22;
1787
1901
  const usesV19CommitYear = rubricVersion === WITAN_RUBRIC_VERSION_V19 || rubricVersion === WITAN_RUBRIC_VERSION_V20 || rubricVersion === WITAN_RUBRIC_VERSION_V21 || rubricVersion === WITAN_RUBRIC_VERSION_V22;
1788
1902
  const usesV20A3ExplicitGaps = rubricVersion === WITAN_RUBRIC_VERSION_V20 || rubricVersion === WITAN_RUBRIC_VERSION_V21 || rubricVersion === WITAN_RUBRIC_VERSION_V22;
@@ -1824,6 +1938,7 @@ function buildWitanInputFromRepoUntracked(options) {
1824
1938
  usesV21ExecutedEscalations,
1825
1939
  usesV22PackageStartEntrypoint,
1826
1940
  reviewableSourceProof,
1941
+ inventoryAbsenceContext,
1827
1942
  scanLimitations
1828
1943
  );
1829
1944
  const archetype = usesV17Detectors && reviewableSourceProof?.passes === true && isV17RescuableAbstention(gatedArchetype.abstentionKind) && coreSignals.some(isMeasuredCriterionSignal) ? {
@@ -1833,7 +1948,10 @@ function buildWitanInputFromRepoUntracked(options) {
1833
1948
  } : gatedArchetype;
1834
1949
  return {
1835
1950
  productSlug: options.productSlug,
1836
- productDisplayName: options.productDisplayName,
1951
+ productDisplayName: sanitizePresentationLine(options.productDisplayName, {
1952
+ fallback: "Repository",
1953
+ maxLength: 120
1954
+ }),
1837
1955
  repo: {
1838
1956
  ...headSha ? { headSha } : {}
1839
1957
  },
@@ -2430,7 +2548,7 @@ function buildNotApplicableSignal(criterionId, reason) {
2430
2548
  notApplicable: true
2431
2549
  };
2432
2550
  }
2433
- function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV18NativeRls, useV19CommitYear, useV20A3ExplicitGaps, useV21ExecutedEscalations, useV22PackageStartEntrypoint, reviewableSourceProof, scanLimitations = /* @__PURE__ */ new Set()) {
2551
+ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV18NativeRls, useV19CommitYear, useV20A3ExplicitGaps, useV21ExecutedEscalations, useV22PackageStartEntrypoint, reviewableSourceProof, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT, scanLimitations = /* @__PURE__ */ new Set()) {
2434
2552
  const signals = [];
2435
2553
  const mono = resolveMonorepoContext(repoPath, scanLimitations);
2436
2554
  const collectCriterion = (criterionId, collect) => withContentReadCriterion(criterionId, collect);
@@ -2442,7 +2560,8 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
2442
2560
  useV27Detectors,
2443
2561
  useV33Detectors,
2444
2562
  useV36Detectors,
2445
- reviewableSourceProof
2563
+ reviewableSourceProof,
2564
+ inventoryAbsenceContext
2446
2565
  )
2447
2566
  );
2448
2567
  const a2Signal = collectCriterion(
@@ -2456,6 +2575,7 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
2456
2575
  useV39Detectors,
2457
2576
  useV47Detectors,
2458
2577
  useV18NativeRls,
2578
+ inventoryAbsenceContext,
2459
2579
  scanLimitations
2460
2580
  )
2461
2581
  );
@@ -2466,7 +2586,8 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
2466
2586
  repoFiles,
2467
2587
  useV27Detectors,
2468
2588
  useV20A3ExplicitGaps,
2469
- useV22PackageStartEntrypoint
2589
+ useV22PackageStartEntrypoint,
2590
+ inventoryAbsenceContext
2470
2591
  )
2471
2592
  );
2472
2593
  const a4Signal = collectCriterion(
@@ -2476,12 +2597,18 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
2476
2597
  repoFiles,
2477
2598
  mono,
2478
2599
  useV27Detectors,
2479
- useV47Detectors
2600
+ useV47Detectors,
2601
+ inventoryAbsenceContext
2480
2602
  )
2481
2603
  );
2482
2604
  const a5Signal = collectCriterion(
2483
2605
  "A5",
2484
- () => collectA5ClaimRealityEvidence(repoPath, repoFiles, useV27Detectors)
2606
+ () => collectA5ClaimRealityEvidence(
2607
+ repoPath,
2608
+ repoFiles,
2609
+ useV27Detectors,
2610
+ inventoryAbsenceContext
2611
+ )
2485
2612
  );
2486
2613
  const b1Signal = buildNotApplicableSignal(
2487
2614
  "B1",
@@ -2532,7 +2659,7 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
2532
2659
  }
2533
2660
  return signals;
2534
2661
  }
2535
- function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, reviewableSourceProof) {
2662
+ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, reviewableSourceProof, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT) {
2536
2663
  const evidence = [];
2537
2664
  const findings = [];
2538
2665
  const allTestFiles = findConcreteTestFiles(
@@ -2629,7 +2756,12 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
2629
2756
  }
2630
2757
  }
2631
2758
  if (evidence.length === 0 && findings.length === 0 && configuredRunnerFiles.length === 0) {
2632
- return reviewableSourceProof?.passes === true && reviewableSourceProof.firstReadablePath ? buildA1AuthenticatedAbsenceSignal(repoPath, reviewableSourceProof) : null;
2759
+ return reviewableSourceProof?.passes === true && reviewableSourceProof.firstReadablePath ? buildA1AuthenticatedAbsenceSignal(
2760
+ repoPath,
2761
+ reviewableSourceProof,
2762
+ repoFiles.length,
2763
+ inventoryAbsenceContext
2764
+ ) : null;
2633
2765
  }
2634
2766
  if (testFiles.length === 0) {
2635
2767
  const fallback = configuredRunnerFiles[0] ? evidenceForRelative(
@@ -2639,11 +2771,21 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
2639
2771
  "Configured test runner"
2640
2772
  ) : useV33Detectors ? void 0 : evidence[0] ?? findings[0]?.evidence;
2641
2773
  if (fallback) {
2642
- findings.push({
2643
- severity: "warning",
2644
- summary: "A test runner is configured, but no concrete test files were detected.",
2645
- evidence: fallback
2646
- });
2774
+ findings.push(
2775
+ inventoryAbsenceFinding({
2776
+ severity: "warning",
2777
+ summaries: {
2778
+ legacy: "A test runner is configured, but no concrete test files were detected.",
2779
+ currentTracked: "A test runner is configured, but the tracked scan-eligible inventory matched no concrete test file.",
2780
+ currentDirectoryWalk: "A test runner is configured, but the bounded directory walk matched no concrete test file."
2781
+ },
2782
+ evidence: fallback,
2783
+ context: inventoryAbsenceContext,
2784
+ inventoryCount: repoFiles.length,
2785
+ patternSet: INVENTORY_SCAN_PATTERN_SETS.a1ConcreteTests,
2786
+ matchCount: 0
2787
+ })
2788
+ );
2647
2789
  }
2648
2790
  } else if (coverageFiles.length === 0 && (!useV33Detectors || configuredRunnerFiles.length > 0)) {
2649
2791
  if (isLeanTestToolchain) {
@@ -2661,11 +2803,21 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
2661
2803
  } else {
2662
2804
  const firstEvidence = evidence[0];
2663
2805
  if (!firstEvidence) return null;
2664
- findings.push({
2665
- severity: "info",
2666
- summary: "Test suite files are present, but no coverage configuration was detected.",
2667
- evidence: firstEvidence
2668
- });
2806
+ findings.push(
2807
+ inventoryAbsenceFinding({
2808
+ severity: "info",
2809
+ summaries: {
2810
+ legacy: "Test suite files are present, but no coverage configuration was detected.",
2811
+ currentTracked: "Test suite files are present, but the tracked scan-eligible inventory matched no coverage configuration.",
2812
+ currentDirectoryWalk: "Test suite files are present, but the bounded directory walk matched no coverage configuration."
2813
+ },
2814
+ evidence: firstEvidence,
2815
+ context: inventoryAbsenceContext,
2816
+ inventoryCount: repoFiles.length,
2817
+ patternSet: INVENTORY_SCAN_PATTERN_SETS.a1CoverageConfiguration,
2818
+ matchCount: 0
2819
+ })
2820
+ );
2669
2821
  }
2670
2822
  }
2671
2823
  const nonHollowShare = measureNonHollowTestShare(
@@ -2738,7 +2890,7 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
2738
2890
  notes: "A1 is detected from real test files, test runner configuration, and optional coverage configuration."
2739
2891
  };
2740
2892
  }
2741
- function buildA1AuthenticatedAbsenceSignal(repoPath, proof) {
2893
+ function buildA1AuthenticatedAbsenceSignal(repoPath, proof, inventoryCount, inventoryAbsenceContext) {
2742
2894
  const sourcePath = proof.firstReadablePath;
2743
2895
  if (!sourcePath) {
2744
2896
  throw new Error("Cejel invariant: a passing reviewable-source proof must have a readable path");
@@ -2753,11 +2905,19 @@ function buildA1AuthenticatedAbsenceSignal(repoPath, proof) {
2753
2905
  criterionId: "A1",
2754
2906
  positiveEvidence: [anchor],
2755
2907
  findings: [
2756
- {
2908
+ inventoryAbsenceFinding({
2757
2909
  severity: "critical",
2758
- summary: WITAN_AUTHENTICATED_A1_ABSENCE_SUMMARY,
2759
- evidence: anchor
2760
- }
2910
+ summaries: {
2911
+ legacy: WITAN_LEGACY_AUTHENTICATED_A1_ABSENCE_SUMMARY,
2912
+ currentTracked: WITAN_AUTHENTICATED_A1_ABSENCE_SUMMARY,
2913
+ currentDirectoryWalk: WITAN_DIRECTORY_WALK_AUTHENTICATED_A1_ABSENCE_SUMMARY
2914
+ },
2915
+ evidence: anchor,
2916
+ context: inventoryAbsenceContext,
2917
+ inventoryCount,
2918
+ patternSet: INVENTORY_SCAN_PATTERN_SETS.a1AuthenticatedAbsence,
2919
+ matchCount: 0
2920
+ })
2761
2921
  ],
2762
2922
  metrics: [
2763
2923
  metric(
@@ -2811,7 +2971,7 @@ function buildA1AuthenticatedAbsenceSignal(repoPath, proof) {
2811
2971
  notes: `V17 authenticated absence: ${proof.eligibleSourceFileCount} criterion-ratable of ${proof.sourceShapedFileCount} source-shaped files; sampled ${proof.sampledFileCount}; ${proof.readableSampledFileCount} representative-text files.`
2812
2972
  };
2813
2973
  }
2814
- function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV18NativeRls, scanLimitations) {
2974
+ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV18NativeRls, inventoryAbsenceContext, scanLimitations) {
2815
2975
  const evidence = [];
2816
2976
  const findings = [];
2817
2977
  const gitignore = repoFiles.find((file) => basename(file) === ".gitignore");
@@ -2964,16 +3124,26 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
2964
3124
  )
2965
3125
  });
2966
3126
  } else if (committedEnvFilePath) {
2967
- findings.push({
2968
- severity: useV33Detectors ? "info" : "warning",
2969
- summary: "A non-template .env file is committed in the current repository tree; no secret-shaped value was detected.",
2970
- evidence: evidenceForRelative(
2971
- repoPath,
2972
- committedEnvFilePath,
2973
- "secret_scan",
2974
- "Committed .env file (no confirmed secret value found)"
2975
- )
2976
- });
3127
+ findings.push(
3128
+ inventoryAbsenceFinding({
3129
+ severity: useV33Detectors ? "info" : "warning",
3130
+ summaries: {
3131
+ legacy: "A non-template .env file is committed in the current repository tree; no secret-shaped value was detected.",
3132
+ currentTracked: "A tracked non-template .env file is present, while the bounded scan over the tracked scan-eligible inventory matched no secret-shaped value.",
3133
+ currentDirectoryWalk: "A non-template .env file is present in the bounded directory walk, which matched no secret-shaped value."
3134
+ },
3135
+ evidence: evidenceForRelative(
3136
+ repoPath,
3137
+ committedEnvFilePath,
3138
+ "secret_scan",
3139
+ "Committed .env file (no confirmed secret value found)"
3140
+ ),
3141
+ context: inventoryAbsenceContext,
3142
+ inventoryCount: repoFiles.length,
3143
+ patternSet: INVENTORY_SCAN_PATTERN_SETS.a2CurrentSecret,
3144
+ matchCount: 0
3145
+ })
3146
+ );
2977
3147
  }
2978
3148
  if (historySecretScan?.evidence) {
2979
3149
  const historyPath = historySecretScan.evidence.path ?? "";
@@ -3005,16 +3175,26 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
3005
3175
  if (hasTenantWithoutRlsGap) {
3006
3176
  const gapEvidence = rlsMigration ?? tenantScopedFile ?? tenantWithoutRlsPremiseFiles[0];
3007
3177
  if (gapEvidence) {
3008
- findings.push({
3009
- severity: "warning",
3010
- summary: "Tenant-scoped schema detected but no row-level-security policies found \u2014 isolation gap.",
3011
- evidence: evidenceForRelative(
3012
- repoPath,
3013
- gapEvidence,
3014
- "artifact",
3015
- "Tenant schema without RLS policies"
3016
- )
3017
- });
3178
+ findings.push(
3179
+ inventoryAbsenceFinding({
3180
+ severity: "warning",
3181
+ summaries: {
3182
+ legacy: "Tenant-scoped schema detected but no row-level-security policies found \u2014 isolation gap.",
3183
+ currentTracked: "A tenant-scoped schema signal is present, while the tracked scan-eligible inventory matched no row-level-security policy.",
3184
+ currentDirectoryWalk: "A tenant-scoped schema signal is present, while the bounded directory walk matched no row-level-security policy."
3185
+ },
3186
+ evidence: evidenceForRelative(
3187
+ repoPath,
3188
+ gapEvidence,
3189
+ "artifact",
3190
+ "Tenant schema without RLS policies"
3191
+ ),
3192
+ context: inventoryAbsenceContext,
3193
+ inventoryCount: repoFiles.length,
3194
+ patternSet: INVENTORY_SCAN_PATTERN_SETS.a2RlsPolicy,
3195
+ matchCount: 0
3196
+ })
3197
+ );
3018
3198
  }
3019
3199
  }
3020
3200
  evidence.push(...cryptoHygiene.evidence);
@@ -3199,7 +3379,7 @@ function collectCryptoHygieneEvidence(repoPath, implFiles, useV39Detectors = fal
3199
3379
  }
3200
3380
  return { evidence, findings, hasSurface, clean: hasSurface && !insecureFound };
3201
3381
  }
3202
- function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, useV20ExplicitGaps = false, useV22PackageStartEntrypoint = false) {
3382
+ function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, useV20ExplicitGaps = false, useV22PackageStartEntrypoint = false, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT) {
3203
3383
  const v20DirectHttpEntrypoint = useV20ExplicitGaps ? findV20DirectHttpServerEntrypointFile(repoPath, repoFiles) : null;
3204
3384
  const v22PackageStartHttpEntrypoint = useV22PackageStartEntrypoint ? findV22PackageStartHttpServerEntrypointFile(repoPath, repoFiles) : null;
3205
3385
  const v20RuntimeContainer = useV20ExplicitGaps ? findRuntimeContainerEntrypointFile(repoPath, repoFiles, true) : null;
@@ -3361,11 +3541,21 @@ function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, us
3361
3541
  if (!workflow && releaseDeployConfigs.length === 0) {
3362
3542
  const firstEvidence = evidence[0];
3363
3543
  if (!firstEvidence) return null;
3364
- findings.push({
3365
- severity: "info",
3366
- summary: "A deployable service surface exists, but no CI or release-deployment automation was detected.",
3367
- evidence: firstEvidence
3368
- });
3544
+ findings.push(
3545
+ inventoryAbsenceFinding({
3546
+ severity: "info",
3547
+ summaries: {
3548
+ legacy: "A deployable service surface exists, but no CI or release-deployment automation was detected.",
3549
+ currentTracked: "A deployable service signal is present, while the tracked scan-eligible inventory matched no CI workflow or release-deployment automation.",
3550
+ currentDirectoryWalk: "A deployable service signal is present, while the bounded directory walk matched no CI workflow or release-deployment automation."
3551
+ },
3552
+ evidence: firstEvidence,
3553
+ context: inventoryAbsenceContext,
3554
+ inventoryCount: repoFiles.length,
3555
+ patternSet: INVENTORY_SCAN_PATTERN_SETS.a3CiOrReleaseDeploy,
3556
+ matchCount: 0
3557
+ })
3558
+ );
3369
3559
  }
3370
3560
  return {
3371
3561
  criterionId: "A3",
@@ -3435,7 +3625,7 @@ function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, us
3435
3625
  ]
3436
3626
  };
3437
3627
  }
3438
- function collectA4DependencyEvidence(repoPath, repoFiles, mono, useV27Detectors, useV47Detectors) {
3628
+ function collectA4DependencyEvidence(repoPath, repoFiles, mono, useV27Detectors, useV47Detectors, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT) {
3439
3629
  const evidence = [];
3440
3630
  const findings = [];
3441
3631
  const manifests = repoFiles.filter(isDependencyManifest);
@@ -3488,11 +3678,26 @@ function collectA4DependencyEvidence(repoPath, repoFiles, mono, useV27Detectors,
3488
3678
  const deployableServiceFiles = useV47Detectors ? findDeployableServicePremiseFiles(repoPath, repoFiles, true) : [];
3489
3679
  const isAppOrService = (useV47Detectors ? deployableServiceFiles.length > 0 : isDeployableService(repoPath, repoFiles, useV27Detectors)) || packagedApplicationFiles.length > 0;
3490
3680
  if (isAppOrService && !hasLockfile && manifest) {
3491
- findings.push({
3492
- severity: "critical",
3493
- summary: "Dependency manifest is present without a detected lockfile \u2014 non-reproducible installs.",
3494
- evidence: evidenceForRelative(repoPath, manifest, "dependency_report", "Dependency manifest")
3495
- });
3681
+ findings.push(
3682
+ inventoryAbsenceFinding({
3683
+ severity: "critical",
3684
+ summaries: {
3685
+ legacy: "Dependency manifest is present without a detected lockfile \u2014 non-reproducible installs.",
3686
+ currentTracked: "Dependency manifest is present, but the tracked scan-eligible inventory matched no recognized lockfile.",
3687
+ currentDirectoryWalk: "Dependency manifest is present, but the bounded directory walk matched no recognized lockfile."
3688
+ },
3689
+ evidence: evidenceForRelative(
3690
+ repoPath,
3691
+ manifest,
3692
+ "dependency_report",
3693
+ "Dependency manifest"
3694
+ ),
3695
+ context: inventoryAbsenceContext,
3696
+ inventoryCount: repoFiles.length + (mono?.sharedFiles.length ?? 0),
3697
+ patternSet: INVENTORY_SCAN_PATTERN_SETS.a4Lockfile,
3698
+ matchCount: 0
3699
+ })
3700
+ );
3496
3701
  }
3497
3702
  if (manifest && hasSuspiciousDependencies(repoPath, manifest, useV47Detectors)) {
3498
3703
  findings.push({
@@ -3600,7 +3805,7 @@ function hasDeclaredVersionConstraint(dependency) {
3600
3805
  if (/[~^<>=!]/.test(trimmed)) return true;
3601
3806
  return /^v?\d/.test(trimmed);
3602
3807
  }
3603
- function collectA5ClaimRealityEvidence(repoPath, repoFiles, useV27Detectors) {
3808
+ function collectA5ClaimRealityEvidence(repoPath, repoFiles, useV27Detectors, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT) {
3604
3809
  const reconciliationArtifacts = useV27Detectors ? findClaimRealityReconciliationArtifacts(repoPath, repoFiles) : [];
3605
3810
  const claimSourceFiles = useV27Detectors ? findClaimSourceFiles(repoFiles) : repoFiles.filter(
3606
3811
  (file) => /^(?:README|readme)\.md$/.test(file) || /^docs\/[^/]+\.(?:md|mdx)$/.test(file)
@@ -3665,15 +3870,23 @@ function collectA5ClaimRealityEvidence(repoPath, repoFiles, useV27Detectors) {
3665
3870
  criterionId: "A5",
3666
3871
  positiveEvidence: evidence,
3667
3872
  findings: reconciliationArtifacts.length > 0 ? [] : [
3668
- {
3873
+ inventoryAbsenceFinding({
3669
3874
  // Proxy-only A5: cap at warning (never critical) — a static proxy is lower-confidence
3670
3875
  // than a curated registry or reconciliation artifact. Downgraded to info when the repo
3671
3876
  // documents honest negative space: that scoping IS a form of claim-reality discipline,
3672
3877
  // not an additional gap on top of the missing dedicated artifact.
3673
3878
  severity: negativeSpaceDoc ? "info" : "warning",
3674
- summary: negativeSpaceDoc ? "Claim source and implementation files are present; no dedicated claim-reality report artifact was supplied, but the repo explicitly documents what it does NOT cover/protect against \u2014 honest scoping, not overclaiming." : "Claim source and implementation files are present, but no dedicated claim-reality report artifact was supplied.",
3675
- evidence: firstEvidence
3676
- }
3879
+ summaries: {
3880
+ legacy: negativeSpaceDoc ? "Claim source and implementation files are present; no dedicated claim-reality report artifact was supplied, but the repo explicitly documents what it does NOT cover/protect against \u2014 honest scoping, not overclaiming." : "Claim source and implementation files are present, but no dedicated claim-reality report artifact was supplied.",
3881
+ currentTracked: negativeSpaceDoc ? "Claim-source and implementation-file signals are present, while the tracked scan-eligible inventory matched no dedicated claim-reality report artifact; the repo separately documents what it does NOT cover or protect against." : "Claim-source and implementation-file signals are present, while the tracked scan-eligible inventory matched no dedicated claim-reality report artifact.",
3882
+ currentDirectoryWalk: negativeSpaceDoc ? "Claim-source and implementation-file signals are present, while the bounded directory walk matched no dedicated claim-reality report artifact; the repo separately documents what it does NOT cover or protect against." : "Claim-source and implementation-file signals are present, while the bounded directory walk matched no dedicated claim-reality report artifact."
3883
+ },
3884
+ evidence: firstEvidence,
3885
+ context: inventoryAbsenceContext,
3886
+ inventoryCount: repoFiles.length,
3887
+ patternSet: INVENTORY_SCAN_PATTERN_SETS.a5ClaimRealityArtifact,
3888
+ matchCount: 0
3889
+ })
3677
3890
  ],
3678
3891
  // Recalibrated (goal_cejel_launch_hardening_combined_2026-07-06, Phase 3 H1):
3679
3892
  // reconciliation_artifact_depth is usually 0 on the generic-proxy path, so its prior 0.3
@@ -4021,7 +4234,7 @@ function collectB6PrivilegedOpsGatingEvidence(repoPath, repoFiles, useV39Detecto
4021
4234
  const isV21ExecutedShape = v21ExecutedEscalationFileSet.has(file);
4022
4235
  findings.push({
4023
4236
  severity: "critical",
4024
- summary: isV21ExecutedShape ? "An authored SQL artifact contains, or a direct database-driver call executes, an administrative role grant, SUPERUSER escalation, or schema-wide table privilege grant with no documented human gate." : "Role-membership GRANT or SUPERUSER escalation executes in code with no documented human gate.",
4237
+ summary: isV21ExecutedShape ? "This file contains an authored administrative SQL statement \u2014 role-membership grant, SUPERUSER escalation, or schema-wide table privilege grant \u2014 or executes one through a direct database-driver call, and does not itself contain text matching the human-gate marker pattern." : "This file executes a role-membership GRANT or SUPERUSER escalation and does not itself contain text matching the human-gate marker pattern.",
4025
4238
  evidence: evidenceForRelative(
4026
4239
  repoPath,
4027
4240
  file,
@@ -4348,10 +4561,20 @@ function parseGitTrackedFiles(repoPath, output, includeHardExcluded) {
4348
4561
  continue;
4349
4562
  }
4350
4563
  try {
4351
- if (!lstatSync(fullPath).isFile()) {
4564
+ const stat = lstatSync(fullPath);
4565
+ if (!stat.isFile()) {
4352
4566
  recordContentSkip(fullPath, "non_regular_file", true);
4353
4567
  continue;
4354
4568
  }
4569
+ if (stat.size > MAX_REPOSITORY_CONTENT_BYTES) {
4570
+ const affectedCriteria = affectedCriteriaForUnavailablePath(file);
4571
+ if (affectedCriteria.length > 0) {
4572
+ recordContentSkipForCriteria(fullPath, "too_large", affectedCriteria, true);
4573
+ } else {
4574
+ recordContentSkip(fullPath, "too_large", true);
4575
+ }
4576
+ continue;
4577
+ }
4355
4578
  } catch (error) {
4356
4579
  recordFilesystemSkip(fullPath, error, false, true);
4357
4580
  continue;
@@ -4363,6 +4586,39 @@ function parseGitTrackedFiles(repoPath, output, includeHardExcluded) {
4363
4586
  }
4364
4587
  return files.sort();
4365
4588
  }
4589
+ function affectedCriteriaForUnavailablePath(file) {
4590
+ const affected = /* @__PURE__ */ new Set();
4591
+ if (isDependencyManifest(file) || isLockfile(file)) affected.add("A4");
4592
+ if (isCiWorkflow(file)) {
4593
+ affected.add("A1");
4594
+ affected.add("A3");
4595
+ affected.add("B2");
4596
+ affected.add("B3");
4597
+ }
4598
+ if (isDeployConfig(file)) {
4599
+ affected.add("A3");
4600
+ affected.add("B2");
4601
+ }
4602
+ if (isTestFile(file)) {
4603
+ affected.add("A1");
4604
+ affected.add("A2");
4605
+ }
4606
+ if (isImplementationFile(file)) {
4607
+ affected.add("A1");
4608
+ affected.add("A2");
4609
+ affected.add("A3");
4610
+ affected.add("B1");
4611
+ affected.add("B6");
4612
+ }
4613
+ if (/\.(?:md|mdx|rst|txt)$/i.test(file)) {
4614
+ affected.add("A5");
4615
+ affected.add("B4");
4616
+ affected.add("B5");
4617
+ affected.add("B6");
4618
+ }
4619
+ if (isAuditFile(file)) affected.add("B5");
4620
+ return [...affected];
4621
+ }
4366
4622
  function directoryInventory(repoPath) {
4367
4623
  const files = [];
4368
4624
  const rootDevice = statSync(repoPath).dev;
@@ -4375,6 +4631,7 @@ function collectRepoFileInventory(repoPath) {
4375
4631
  return {
4376
4632
  repoFiles: parseGitTrackedFiles(repoPath, tracked.stdout, false),
4377
4633
  inventoryFiles: parseGitTrackedFiles(repoPath, tracked.stdout, true),
4634
+ repoFileInventoryScope: "tracked-scan-eligible",
4378
4635
  scanLimitations: []
4379
4636
  };
4380
4637
  }
@@ -4482,8 +4739,13 @@ function visitRepoDir(repoPath, dirPath, files, rootDevice) {
4482
4739
  continue;
4483
4740
  }
4484
4741
  const repoRelativePath = relative(repoPath, fullPath);
4485
- if (size > 512e3) {
4486
- recordContentSkip(fullPath, "too_large");
4742
+ if (size > MAX_REPOSITORY_CONTENT_BYTES) {
4743
+ const affectedCriteria = affectedCriteriaForUnavailablePath(repoRelativePath);
4744
+ if (affectedCriteria.length > 0) {
4745
+ recordContentSkipForCriteria(fullPath, "too_large", affectedCriteria);
4746
+ } else {
4747
+ recordContentSkip(fullPath, "too_large");
4748
+ }
4487
4749
  continue;
4488
4750
  }
4489
4751
  if (!isPotentialContentPath(repoRelativePath)) {
@@ -6475,12 +6737,11 @@ function hasEnvPathInGitHistory(repoPath, scanLimitations) {
6475
6737
 
6476
6738
  // src/witan/public-scan.ts
6477
6739
  init_esm_shims();
6478
- import { resolve as resolve2 } from "path";
6479
6740
 
6480
6741
  // src/witan/ingest.ts
6481
6742
  init_esm_shims();
6482
- import { readFileSync as readFileSync4, readdirSync as readdirSync2 } from "fs";
6483
- import { join as join2 } from "path";
6743
+ import { lstatSync as lstatSync3, opendirSync, readFileSync as readFileSync4, realpathSync as realpathSync3 } from "fs";
6744
+ import { join as join2, resolve as resolve3 } from "path";
6484
6745
 
6485
6746
  // src/witan/finding-limits.ts
6486
6747
  init_esm_shims();
@@ -6504,49 +6765,118 @@ function clampFinding(finding) {
6504
6765
  // src/witan/generic-adapter.ts
6505
6766
  init_esm_shims();
6506
6767
  import { readFileSync } from "fs";
6507
- var VALID_DIMENSIONS = /* @__PURE__ */ new Set([
6508
- "A1",
6509
- "A2",
6510
- "A3",
6511
- "A4",
6512
- "A5",
6513
- "B1",
6514
- "B2",
6515
- "B3",
6516
- "B4",
6517
- "B5",
6518
- "B6"
6519
- ]);
6520
- var VALID_SEVERITIES = /* @__PURE__ */ new Set(["critical", "warning", "info"]);
6768
+
6769
+ // src/witan/ingest-files.ts
6770
+ init_esm_shims();
6771
+ import { lstatSync as lstatSync2, realpathSync as realpathSync2 } from "fs";
6772
+ import { dirname as dirname2, isAbsolute, relative as relative2, resolve as resolve2, sep as sep2 } from "path";
6773
+ var MAX_INGEST_DOCUMENT_BYTES = 8 * 1024 * 1024;
6774
+ function isContainedPath(candidate, directory) {
6775
+ const relativePath = relative2(directory, candidate);
6776
+ return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${sep2}`) && !isAbsolute(relativePath);
6777
+ }
6778
+ function isMissingIngestPath(error) {
6779
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
6780
+ }
6781
+ function resolveIngestFilePath(filePath, inputDirectory = dirname2(filePath)) {
6782
+ const lexicalDirectory = resolve2(inputDirectory);
6783
+ const lexicalCandidate = resolve2(filePath);
6784
+ const directoryStat = lstatSync2(lexicalDirectory);
6785
+ if (directoryStat.isSymbolicLink()) {
6786
+ throw new Error(`Cejel: refusing symlinked ingest directory: ${inputDirectory}`);
6787
+ }
6788
+ if (!directoryStat.isDirectory()) {
6789
+ throw new Error(`Cejel: refusing non-directory ingest boundary: ${inputDirectory}`);
6790
+ }
6791
+ const candidateStat = lstatSync2(lexicalCandidate);
6792
+ if (candidateStat.isSymbolicLink()) {
6793
+ throw new Error(`Cejel: refusing symlinked ingest file: ${filePath}`);
6794
+ }
6795
+ if (!candidateStat.isFile()) {
6796
+ throw new Error(`Cejel: refusing non-regular ingest file: ${filePath}`);
6797
+ }
6798
+ if (candidateStat.size > MAX_INGEST_DOCUMENT_BYTES) {
6799
+ throw new Error(
6800
+ `Cejel: ingest file exceeds the ${MAX_INGEST_DOCUMENT_BYTES.toLocaleString("en-US")} byte budget: ${filePath}`
6801
+ );
6802
+ }
6803
+ const resolvedDirectory = realpathSync2(lexicalDirectory);
6804
+ const resolvedCandidate = realpathSync2(lexicalCandidate);
6805
+ if (!isContainedPath(resolvedCandidate, resolvedDirectory)) {
6806
+ throw new Error(`Cejel: refusing ingest file outside its resolved input directory: ${filePath}`);
6807
+ }
6808
+ return resolvedCandidate;
6809
+ }
6810
+ function isResolvedIngestPathContained(candidate, directory) {
6811
+ return isContainedPath(candidate, directory);
6812
+ }
6813
+
6814
+ // src/witan/generic-adapter.ts
6815
+ var CEJEL_GENERIC_INGEST_CONTRACT_VERSION = "1.0";
6816
+ var CEJEL_GENERIC_INGEST_CONTRACT_MAJOR = 1;
6817
+ var GenericFindingV1Schema = external_exports.object({
6818
+ ruleId: external_exports.string().min(1),
6819
+ severity: external_exports.enum(["critical", "warning", "info"]),
6820
+ message: external_exports.string().min(1),
6821
+ location: external_exports.string().min(1).optional()
6822
+ }).passthrough();
6823
+ var GenericSignalV1Schema = external_exports.object({
6824
+ dimension: external_exports.enum(["A1", "A2", "A3", "A4", "A5", "B1", "B2", "B3", "B4", "B5", "B6"]),
6825
+ weight: external_exports.number().min(0).max(1).default(0.5),
6826
+ findings: external_exports.array(GenericFindingV1Schema).min(1)
6827
+ }).passthrough();
6828
+ var GenericSignalDocumentV1Schema = external_exports.object({
6829
+ version: external_exports.string().regex(/^1\.\d+$/),
6830
+ tool: external_exports.string().trim().min(1),
6831
+ signals: external_exports.array(GenericSignalV1Schema)
6832
+ }).passthrough();
6521
6833
  function isGenericSignalDocument(raw) {
6522
6834
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return false;
6523
6835
  const doc = raw;
6524
6836
  return typeof doc.tool === "string" && doc.tool.length > 0 && Array.isArray(doc.signals);
6525
6837
  }
6838
+ function assertSupportedContractVersion(version) {
6839
+ if (typeof version !== "string") {
6840
+ throw new Error(
6841
+ `Cejel: generic ingest contract version is required; expected "version": "${CEJEL_GENERIC_INGEST_CONTRACT_VERSION}". Refusing to guess an unversioned contract.`
6842
+ );
6843
+ }
6844
+ const match = /^(\d+)\.(\d+)$/.exec(version);
6845
+ if (!match) {
6846
+ throw new Error(
6847
+ `Cejel: invalid generic ingest contract version "${version}"; expected <major>.<minor> (current ${CEJEL_GENERIC_INGEST_CONTRACT_VERSION}).`
6848
+ );
6849
+ }
6850
+ const major = Number(match[1]);
6851
+ if (major !== CEJEL_GENERIC_INGEST_CONTRACT_MAJOR) {
6852
+ throw new Error(
6853
+ `Cejel: unsupported generic ingest contract major version ${major} (declared "${version}"); this Cejel build supports major ${CEJEL_GENERIC_INGEST_CONTRACT_MAJOR}. Refusing to guess.`
6854
+ );
6855
+ }
6856
+ }
6857
+ function formatValidationIssues(error) {
6858
+ return error.issues.map((issue) => `${issue.path.length > 0 ? issue.path.join(".") : "<document>"}: ${issue.message}`).join("; ");
6859
+ }
6526
6860
  function parseGenericJson(raw) {
6527
6861
  if (!isGenericSignalDocument(raw)) return [];
6528
- const source = raw.tool ?? "unknown";
6529
- const signals = [];
6530
- for (const signal of raw.signals ?? []) {
6531
- const dimension = signal.dimension;
6532
- if (!dimension || !VALID_DIMENSIONS.has(dimension)) continue;
6533
- const weight = typeof signal.weight === "number" && Number.isFinite(signal.weight) ? Math.min(Math.max(signal.weight, 0), 1) : 0.5;
6534
- const findings = [];
6535
- for (const finding of signal.findings ?? []) {
6536
- const severity = finding.severity;
6537
- if (!severity || !VALID_SEVERITIES.has(severity)) continue;
6538
- const ruleId = finding.ruleId ?? "unknown";
6539
- findings.push({
6540
- ruleId,
6541
- severity,
6542
- message: finding.message ?? ruleId,
6543
- ...finding.location ? { location: finding.location } : {}
6544
- });
6545
- }
6546
- if (findings.length === 0) continue;
6547
- signals.push({ source, dimension, weight, findings });
6862
+ assertSupportedContractVersion(raw.version);
6863
+ const parsed = GenericSignalDocumentV1Schema.safeParse(raw);
6864
+ if (!parsed.success) {
6865
+ throw new Error(
6866
+ `Cejel: invalid generic ingest contract v${raw.version}: ${formatValidationIssues(parsed.error)}`
6867
+ );
6548
6868
  }
6549
- return signals;
6869
+ return parsed.data.signals.map((signal) => ({
6870
+ source: parsed.data.tool,
6871
+ dimension: signal.dimension,
6872
+ weight: signal.weight,
6873
+ findings: signal.findings.map((finding) => ({
6874
+ ruleId: finding.ruleId,
6875
+ severity: finding.severity,
6876
+ message: finding.message,
6877
+ ...finding.location ? { location: finding.location } : {}
6878
+ }))
6879
+ }));
6550
6880
  }
6551
6881
 
6552
6882
  // src/witan/sarif-adapter.ts
@@ -6808,30 +7138,35 @@ function rawToolName(raw) {
6808
7138
  }
6809
7139
  var MAX_INGEST_SOURCE_LENGTH = 120;
6810
7140
  function sanitizeIngestSource(source) {
6811
- const withoutControls = source.replace(/[\p{Cc}\p{Cf}]/gu, "").trim();
6812
- const safeSource = withoutControls.length > 0 ? withoutControls : "unknown";
6813
- const codePoints = Array.from(safeSource);
6814
- if (codePoints.length <= MAX_INGEST_SOURCE_LENGTH) return safeSource;
6815
- return `${codePoints.slice(0, MAX_INGEST_SOURCE_LENGTH - 3).join("")}...`;
7141
+ return sanitizePresentationLine(source, {
7142
+ fallback: "unknown",
7143
+ maxLength: MAX_INGEST_SOURCE_LENGTH
7144
+ });
6816
7145
  }
6817
7146
  function parseIngestFile(filePath, options = {}) {
7147
+ const resolvedFilePath = resolveIngestFilePath(filePath);
6818
7148
  let raw;
6819
7149
  try {
6820
- raw = JSON.parse(stripBom(readFileSync4(filePath, "utf8")));
7150
+ raw = JSON.parse(stripBom(readFileSync4(resolvedFilePath, "utf8")));
6821
7151
  } catch (error) {
6822
7152
  const message = error instanceof Error ? error.message : String(error);
6823
7153
  throw new Error(`Cejel: could not parse ingest file as JSON: ${filePath} (${message})`);
6824
7154
  }
7155
+ const rawCount = countRawEntries(raw);
7156
+ if (options.maxFindingCandidates !== void 0 && rawCount > options.maxFindingCandidates) {
7157
+ throw new Error(
7158
+ `Cejel: ${rawCount.toLocaleString("en-US")} ingest finding candidates exceed the ${options.maxFindingCandidates.toLocaleString("en-US")} remaining retained ingest finding budget; refusing before adapter materialization.`
7159
+ );
7160
+ }
6825
7161
  let signals;
6826
7162
  if (looksLikeSarif(raw)) signals = parseSarifJson(raw, options.extraSarifDimensionRules);
6827
7163
  else if (looksLikeScorecard(raw)) signals = parseScorecardJson(raw);
6828
7164
  else if (isGenericSignalDocument(raw)) signals = parseGenericJson(raw);
6829
7165
  if (!signals) {
6830
7166
  throw new Error(
6831
- `Cejel: unrecognized ingest file format: ${filePath} \u2014 expected SARIF (a "runs" array), OpenSSF Scorecard JSON (a "checks" array), or the generic Cejel external-signal shape (a "tool" string + "signals" array). See the cejel README "Aggregate your scanners".`
7167
+ `Cejel: unrecognized ingest file format: ${filePath} \u2014 expected SARIF (a "runs" array), OpenSSF Scorecard JSON (a "checks" array), or the versioned generic Cejel external-signal shape (a "version" string + "tool" string + "signals" array). See the cejel README "Aggregate your scanners".`
6832
7168
  );
6833
7169
  }
6834
- const rawCount = countRawEntries(raw);
6835
7170
  const mappedCount = signals.reduce((sum, signal) => sum + signal.findings.length, 0);
6836
7171
  if (rawCount > 0 && mappedCount === 0) {
6837
7172
  process.stderr.write(
@@ -6846,30 +7181,83 @@ function parseIngestFile(filePath, options = {}) {
6846
7181
  findings: signal.findings.map(clampFinding)
6847
7182
  }));
6848
7183
  }
6849
- function expandIngestPattern(pattern) {
7184
+ function expandIngestPattern(pattern, maxMatches = Number.POSITIVE_INFINITY) {
6850
7185
  if (!pattern.includes("*")) return [pattern];
6851
7186
  const lastSlash = pattern.lastIndexOf("/");
6852
7187
  const dir = lastSlash === -1 ? "." : pattern.slice(0, lastSlash);
6853
7188
  const filePattern = lastSlash === -1 ? pattern : pattern.slice(lastSlash + 1);
6854
7189
  const regexSource = filePattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
6855
7190
  const regex = new RegExp(`^${regexSource}$`);
6856
- let entries;
7191
+ const entries = [];
6857
7192
  try {
6858
- entries = readdirSync2(dir);
6859
- } catch {
7193
+ const directoryStat = lstatSync3(resolve3(dir));
7194
+ if (directoryStat.isSymbolicLink()) {
7195
+ throw new Error(`Cejel: refusing symlinked ingest directory: ${dir}`);
7196
+ }
7197
+ if (!directoryStat.isDirectory()) return [];
7198
+ const directory = opendirSync(dir);
7199
+ try {
7200
+ for (; ; ) {
7201
+ const entry = directory.readSync();
7202
+ if (!entry) break;
7203
+ if (!regex.test(entry.name)) continue;
7204
+ entries.push(entry.name);
7205
+ if (entries.length >= maxMatches) break;
7206
+ }
7207
+ } finally {
7208
+ directory.closeSync();
7209
+ }
7210
+ } catch (error) {
7211
+ if (!isMissingIngestPath(error)) throw error;
6860
7212
  return [];
6861
7213
  }
6862
- return entries.filter((name) => regex.test(name)).sort().map((name) => join2(dir, name));
7214
+ return entries.sort().map((name) => {
7215
+ const candidate = join2(dir, name);
7216
+ resolveIngestFilePath(candidate, dir);
7217
+ return candidate;
7218
+ });
6863
7219
  }
6864
- function discoverIngestInputs(repoPath) {
7220
+ function discoverIngestInputs(repoPath, maxMatches = Number.POSITIVE_INFINITY) {
7221
+ const resolvedRoot = realpathSync3(repoPath);
7222
+ const cejelDir = join2(repoPath, ".cejel");
6865
7223
  const inputsDir = join2(repoPath, ".cejel", "inputs");
6866
- let entries;
7224
+ const entries = [];
6867
7225
  try {
6868
- entries = readdirSync2(inputsDir);
6869
- } catch {
7226
+ const cejelStat = lstatSync3(cejelDir);
7227
+ if (cejelStat.isSymbolicLink()) {
7228
+ throw new Error(`Cejel: refusing symlinked ingest path component: ${cejelDir}`);
7229
+ }
7230
+ if (!cejelStat.isDirectory()) {
7231
+ throw new Error(`Cejel: refusing non-directory ingest path component: ${cejelDir}`);
7232
+ }
7233
+ const inputsStat = lstatSync3(inputsDir);
7234
+ if (inputsStat.isSymbolicLink()) {
7235
+ throw new Error(`Cejel: refusing symlinked ingest path component: ${inputsDir}`);
7236
+ }
7237
+ if (!inputsStat.isDirectory()) {
7238
+ throw new Error(`Cejel: refusing non-directory ingest path component: ${inputsDir}`);
7239
+ }
7240
+ const resolvedInputs = realpathSync3(inputsDir);
7241
+ if (!isResolvedIngestPathContained(resolvedInputs, resolvedRoot)) {
7242
+ throw new Error(`Cejel: refusing auto-discovery directory outside the repository root.`);
7243
+ }
7244
+ const directory = opendirSync(inputsDir);
7245
+ try {
7246
+ for (; ; ) {
7247
+ const entry = directory.readSync();
7248
+ if (!entry) break;
7249
+ if (!/\.(sarif|json)$/i.test(entry.name)) continue;
7250
+ entries.push(entry.name);
7251
+ if (entries.length >= maxMatches) break;
7252
+ }
7253
+ } finally {
7254
+ directory.closeSync();
7255
+ }
7256
+ } catch (error) {
7257
+ if (!isMissingIngestPath(error)) throw error;
6870
7258
  return [];
6871
7259
  }
6872
- return entries.filter((name) => /\.(sarif|json)$/i.test(name)).sort().map((name) => join2(inputsDir, name));
7260
+ return entries.sort().map((name) => resolveIngestFilePath(join2(inputsDir, name), inputsDir));
6873
7261
  }
6874
7262
 
6875
7263
  // src/witan/scoring.ts
@@ -7281,7 +7669,11 @@ function scoreMetrics(metrics) {
7281
7669
  function capScoreForFindings(score, findings, rubricVersion, criterionId) {
7282
7670
  if (findings.some((finding) => finding.severity === "critical")) {
7283
7671
  const criticalFindings = findings.filter(({ severity }) => severity === "critical");
7284
- const authenticatedA1Absence = usesV17DetectorClosure2(rubricVersion) && criterionId === "A1" && criticalFindings.length === 1 && criticalFindings[0]?.summary === WITAN_AUTHENTICATED_A1_ABSENCE_SUMMARY;
7672
+ const authenticatedA1Absence = usesV17DetectorClosure2(rubricVersion) && criterionId === "A1" && criticalFindings.length === 1 && [
7673
+ WITAN_AUTHENTICATED_A1_ABSENCE_SUMMARY,
7674
+ WITAN_DIRECTORY_WALK_AUTHENTICATED_A1_ABSENCE_SUMMARY,
7675
+ WITAN_LEGACY_AUTHENTICATED_A1_ABSENCE_SUMMARY
7676
+ ].includes(criticalFindings[0]?.summary ?? "");
7285
7677
  return authenticatedA1Absence ? Math.min(score, 1.4) : 1.4;
7286
7678
  }
7287
7679
  if (findings.some((finding) => finding.severity === "warning")) return Math.min(score, 2.4);
@@ -7553,6 +7945,8 @@ function roundScore(score) {
7553
7945
  }
7554
7946
 
7555
7947
  // src/witan/public-scan.ts
7948
+ var MAX_INGEST_DOCUMENTS = 128;
7949
+ var MAX_RETAINED_INGEST_FINDINGS = 1e4;
7556
7950
  function scoreRepoWithPublicCejel(options) {
7557
7951
  if (options.rubricVersion !== void 0) {
7558
7952
  assertSelectableRubricVersion(options.rubricVersion);
@@ -7580,36 +7974,58 @@ function scoreRepoWithPublicCejel(options) {
7580
7974
  }
7581
7975
  function resolvePublicIngestSignals(options) {
7582
7976
  const seen = /* @__PURE__ */ new Set();
7583
- const explicitFiles = [];
7977
+ const documents = [];
7978
+ const addDocument = (file, provenance) => {
7979
+ const resolved = resolveIngestFilePath(file);
7980
+ if (seen.has(resolved)) return;
7981
+ seen.add(resolved);
7982
+ documents.push({ file: resolved, provenance });
7983
+ if (documents.length > MAX_INGEST_DOCUMENTS) {
7984
+ throw new Error(
7985
+ `Cejel: ${documents.length} unique ingest documents exceed the ${MAX_INGEST_DOCUMENTS} ingest document budget; refusing to produce a partial certificate.`
7986
+ );
7987
+ }
7988
+ };
7584
7989
  for (const pattern of options.ingestPatterns ?? []) {
7585
- const matches = expandIngestPattern(pattern);
7990
+ const matches = expandIngestPattern(pattern, MAX_INGEST_DOCUMENTS + 1);
7586
7991
  if (matches.length === 0 && options.warnOnEmptyIngestMatch) {
7587
7992
  process.stderr.write(`Cejel: --ingest pattern matched no files: ${pattern}
7588
7993
  `);
7589
7994
  }
7590
7995
  for (const match of matches) {
7591
- const resolved = resolve2(match);
7592
- if (seen.has(resolved)) continue;
7593
- seen.add(resolved);
7594
- explicitFiles.push(match);
7996
+ addDocument(match, "operator_supplied");
7595
7997
  }
7596
7998
  }
7597
- const signals = explicitFiles.flatMap(
7598
- (file) => parseIngestFile(file, { provenance: "operator_supplied" })
7599
- );
7600
- if (!options.autoDiscoverIngest) return signals;
7601
- for (const discovered of discoverIngestInputs(options.repoPath)) {
7602
- const resolved = resolve2(discovered);
7603
- if (seen.has(resolved)) continue;
7604
- seen.add(resolved);
7605
- signals.push(...parseIngestFile(discovered, { provenance: "auto_discovered" }));
7999
+ if (options.autoDiscoverIngest) {
8000
+ for (const discovered of discoverIngestInputs(
8001
+ options.repoPath,
8002
+ MAX_INGEST_DOCUMENTS + 1
8003
+ )) {
8004
+ addDocument(discovered, "auto_discovered");
8005
+ }
8006
+ }
8007
+ const signals = [];
8008
+ let retainedFindings = 0;
8009
+ for (const document of documents) {
8010
+ const parsed = parseIngestFile(document.file, {
8011
+ maxFindingCandidates: MAX_RETAINED_INGEST_FINDINGS - retainedFindings,
8012
+ provenance: document.provenance
8013
+ });
8014
+ const documentFindings = parsed.reduce((count, signal) => count + signal.findings.length, 0);
8015
+ retainedFindings += documentFindings;
8016
+ if (retainedFindings > MAX_RETAINED_INGEST_FINDINGS) {
8017
+ throw new Error(
8018
+ `Cejel: ${retainedFindings.toLocaleString("en-US")} retained findings exceed the ${MAX_RETAINED_INGEST_FINDINGS.toLocaleString("en-US")} retained ingest finding budget; refusing to truncate evidence.`
8019
+ );
8020
+ }
8021
+ signals.push(...parsed);
7606
8022
  }
7607
8023
  return signals;
7608
8024
  }
7609
8025
 
7610
8026
  // src/product-identity.ts
7611
8027
  init_esm_shims();
7612
- import { existsSync, readFileSync as readFileSync5 } from "fs";
8028
+ import { existsSync, lstatSync as lstatSync4, readFileSync as readFileSync5 } from "fs";
7613
8029
  import { basename as basename2, join as join3 } from "path";
7614
8030
  var SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$/;
7615
8031
  var FALLBACK_SLUG = "repo";
@@ -7617,7 +8033,10 @@ var FALLBACK_DISPLAY_NAME = "Repository";
7617
8033
  function deriveProductIdentity(repoPath, productName) {
7618
8034
  const packageJsonPath = join3(repoPath, "package.json");
7619
8035
  const raw = productName ?? readPackageName(packageJsonPath) ?? basename2(repoPath);
7620
- const displayName = raw.trim().length > 0 ? raw.trim() : FALLBACK_DISPLAY_NAME;
8036
+ const displayName = sanitizePresentationLine(raw, {
8037
+ fallback: FALLBACK_DISPLAY_NAME,
8038
+ maxLength: 120
8039
+ });
7621
8040
  return {
7622
8041
  productSlug: slugify(raw),
7623
8042
  productDisplayName: displayName
@@ -7626,6 +8045,8 @@ function deriveProductIdentity(repoPath, productName) {
7626
8045
  function readPackageName(packageJsonPath) {
7627
8046
  if (!existsSync(packageJsonPath)) return void 0;
7628
8047
  try {
8048
+ const stat = lstatSync4(packageJsonPath);
8049
+ if (!stat.isFile() || stat.size > MAX_REPOSITORY_CONTENT_BYTES) return void 0;
7629
8050
  const parsed = JSON.parse(readFileSync5(packageJsonPath, "utf8"));
7630
8051
  if (typeof parsed === "object" && parsed !== null && "name" in parsed) {
7631
8052
  const name = parsed.name;