@cejel/cejel 0.4.7 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -93,7 +93,7 @@ if (-not $expected -or $actual -ne $expected.ToLowerInvariant()) {
93
93
  ```
94
94
 
95
95
  > **Windows signing status:** `cejel-Windows-x86_64.exe` is intentionally unsigned in
96
- > 0.4.7 and may trigger Microsoft SmartScreen. The release build removes Node's inherited
96
+ > 0.4.8 and may trigger Microsoft SmartScreen. The release build removes Node's inherited
97
97
  > signature before SEA injection and fails unless Windows reports the result as `NotSigned`;
98
98
  > it does not ship an invalid signature. Before running it, verify `SHA256SUMS` and the
99
99
  > GitHub build-provenance attestation. Each binary also has an attached SPDX SBOM and an
@@ -116,7 +116,7 @@ docker run --rm --network=none -v "$PWD:/w" -w /w -v "$PWD/cejel:/cejel:ro" debi
116
116
  npx @cejel/cejel@latest .
117
117
  ```
118
118
 
119
- > **Distribution note:** Cejel `0.4.7` is the coordinated release version for npm,
119
+ > **Distribution note:** Cejel `0.4.8` is the coordinated release version for npm,
120
120
  > standalone binaries, Docker/OCI, GitHub Action, Homebrew, and MCP Registry.
121
121
 
122
122
  `npx` can reuse a stale cached package. Force the current npm release with the `@latest`
@@ -178,16 +178,16 @@ gh attestation verify ./cejel-Windows-x86_64.exe -R BargLabs/cejel
178
178
  This is cryptographically signed provenance. It is distinct from Apple Developer ID or
179
179
  Microsoft Authenticode code-signing.
180
180
 
181
- **Docker / OCI.** The current container release is `0.4.7`:
181
+ **Docker / OCI.** The current container release is `0.4.8`:
182
182
 
183
183
  ```bash
184
- docker run --rm -i -v "$PWD:/workspace:ro" ghcr.io/barglabs/cejel:0.4.7
184
+ docker run --rm -i -v "$PWD:/workspace:ro" ghcr.io/barglabs/cejel:0.4.8
185
185
  ```
186
186
 
187
187
  The image defaults to `cejel-mcp` over stdio. To use the CLI instead:
188
188
 
189
189
  ```bash
190
- docker run --rm -v "$PWD:/workspace:ro" --entrypoint cejel ghcr.io/barglabs/cejel:0.4.7 .
190
+ docker run --rm -v "$PWD:/workspace:ro" --entrypoint cejel ghcr.io/barglabs/cejel:0.4.8 .
191
191
  ```
192
192
 
193
193
  The OCI image carries an SBOM, maximum-mode build provenance, and a signed registry
@@ -519,7 +519,7 @@ The OCI image is an alternative when Docker is the preferred execution boundary.
519
519
  the host path with the repository OpenClaw should allow Cejel to read:
520
520
 
521
521
  ```bash
522
- npx -y openclaw@latest mcp set cejel-oci '{"command":"docker","args":["run","--rm","-i","-v","/absolute/path/to/repo:/workspace:ro","ghcr.io/barglabs/cejel:0.4.7"]}'
522
+ npx -y openclaw@latest mcp set cejel-oci '{"command":"docker","args":["run","--rm","-i","-v","/absolute/path/to/repo:/workspace:ro","ghcr.io/barglabs/cejel:0.4.8"]}'
523
523
  npx -y openclaw@latest mcp doctor cejel-oci --probe
524
524
  ```
525
525
 
@@ -5,20 +5,20 @@ import {
5
5
  deriveProductIdentity,
6
6
  runCejelScan,
7
7
  serializeWitanReport
8
- } from "../chunk-QTLLASOO.js";
8
+ } from "../chunk-6R3VJGJ2.js";
9
9
  import {
10
10
  listCejelLlmPackFiles
11
- } from "../chunk-EP7YYWEP.js";
12
- import "../chunk-OIGCYULH.js";
11
+ } from "../chunk-ERNYMFH4.js";
12
+ import "../chunk-KNQBI3IX.js";
13
13
  import {
14
14
  external_exports
15
- } from "../chunk-UYJBSQCV.js";
15
+ } from "../chunk-J6R5OHN7.js";
16
16
  import {
17
17
  init_esm_shims,
18
18
  readRepoText,
19
19
  recordContentSkip,
20
20
  recordFilesystemSkip
21
- } from "../chunk-ZNVILRHK.js";
21
+ } from "../chunk-V3B2V4WA.js";
22
22
 
23
23
  // src/calibration/llm-detector.ts
24
24
  init_esm_shims();
@@ -5,24 +5,24 @@ import {
5
5
  describeGitFailure,
6
6
  execGit,
7
7
  isExpectedGitAbsence
8
- } from "./chunk-OIGCYULH.js";
8
+ } from "./chunk-KNQBI3IX.js";
9
9
  import {
10
10
  external_exports
11
- } from "./chunk-UYJBSQCV.js";
11
+ } from "./chunk-J6R5OHN7.js";
12
12
  import {
13
13
  MAX_REPOSITORY_CONTENT_BYTES,
14
+ classifyAbstentionReason,
14
15
  contentReadSignalKey,
15
16
  init_esm_shims,
16
17
  readRepoText,
17
18
  readRepoTextPrefix,
18
19
  recordContentSkip,
19
- recordContentSkipForCriteria,
20
20
  recordFilesystemSkip,
21
21
  trackContentReads,
22
22
  withContentReadCriterion,
23
23
  withContentReadSignal,
24
24
  withContentReadSignals
25
- } from "./chunk-ZNVILRHK.js";
25
+ } from "./chunk-V3B2V4WA.js";
26
26
 
27
27
  // src/witan/schemas.ts
28
28
  init_esm_shims();
@@ -162,6 +162,13 @@ var WitanCriterionMetricSchema = external_exports.object({
162
162
  condition: external_exports.string().min(1).max(240).optional()
163
163
  }).strict().optional()
164
164
  }).strict();
165
+ var WitanContentReadSkipReasonSchema = external_exports.enum([
166
+ "unreadable",
167
+ "too_large",
168
+ "excluded_by_extension",
169
+ "denied_path",
170
+ "non_regular_file"
171
+ ]);
165
172
  var WitanCriterionSignalSchema = external_exports.object({
166
173
  criterionId: WitanCriterionIdSchema,
167
174
  positiveEvidence: external_exports.array(WitanEvidencePointerSchema).default([]),
@@ -171,9 +178,19 @@ var WitanCriterionSignalSchema = external_exports.object({
171
178
  notApplicable: external_exports.literal(true).optional(),
172
179
  // A collector reached relevant files but could not read their contents. This is a
173
180
  // measurement gap, never negative evidence: scoreCriterion maps it to insufficient_data.
174
- insufficientData: external_exports.literal(true).optional()
181
+ insufficientData: external_exports.literal(true).optional(),
182
+ // Why insufficientData is true. Omitted (or 'unreadable'/'denied_path') means an
183
+ // environmental read failure — evidence Cejel expected to see and could not; that stays in
184
+ // the composite denominator at score 0 so losing evidence never flatters the certificate
185
+ // (ADR-0001 does not cover this case — it is genuine evidence loss). 'too_large',
186
+ // 'excluded_by_extension', and 'non_regular_file' are self-imposed coverage limits Cejel
187
+ // chose itself: those are excluded from the composite exactly like ordinary
188
+ // insufficient_data (ADR-0001: coverage is disclosed, never discounts a score).
189
+ insufficientDataReason: WitanContentReadSkipReasonSchema.optional()
175
190
  }).strict().refine((signal) => !(signal.notApplicable && signal.insufficientData), {
176
191
  message: "A criterion cannot be both not applicable and insufficient data."
192
+ }).refine((signal) => signal.insufficientDataReason === void 0 || signal.insufficientData === true, {
193
+ message: "insufficientDataReason may only be set alongside insufficientData: true."
177
194
  });
178
195
  var WitanContentReadSummarySchema = external_exports.object({
179
196
  skipped: external_exports.number().int().nonnegative(),
@@ -301,7 +318,14 @@ var WitanReportCommonSchema = external_exports.object({
301
318
  consumedSignals: external_exports.array(WitanConsumedSignalSummarySchema).optional(),
302
319
  archetype: WitanRepoArchetypeSchema.optional(),
303
320
  scanLimitations: external_exports.array(external_exports.string().min(1).max(1e3)).max(16).optional(),
304
- contentReadSummary: WitanContentReadSummarySchema.optional()
321
+ contentReadSummary: WitanContentReadSummarySchema.optional(),
322
+ // The @cejel/cejel version that produced this report. Absent on reports from before this
323
+ // field existed and on any caller that does not supply one (createWitanReport never defaults
324
+ // it). Constant within an installed version, so re-running the same version on the same
325
+ // commit stays byte-stable (the exact property v0.3.1 removed generatedAt to protect); it
326
+ // differs across versions, which lets a consumer key incremental reuse on it
327
+ // (goal_cejel_0_4_8_abstention_scoring_fix_2026-09-08).
328
+ toolVersion: external_exports.string().min(1).max(80).optional()
305
329
  });
306
330
  var WitanScoredReportSchema = WitanReportCommonSchema.extend({
307
331
  verdict: WitanScoredVerdictSchema,
@@ -342,7 +366,7 @@ var WitanReportSchema = external_exports.preprocess((value) => {
342
366
  }, StrictWitanReportSchema);
343
367
  var WITAN_ATTESTATION_STATEMENT_TYPE = "https://in-toto.io/Statement/v1";
344
368
  var WITAN_ATTESTATION_PREDICATE_TYPE = "https://cejel.dev/attestations/scan/v1";
345
- var WITAN_REPORT_FORMAT_VERSION = "1.0";
369
+ var WITAN_REPORT_FORMAT_VERSION = "1.1";
346
370
  var WitanAttestationOutcomeSchema = external_exports.discriminatedUnion("status", [
347
371
  external_exports.object({
348
372
  status: external_exports.literal("scored"),
@@ -2007,29 +2031,56 @@ function inventoryAbsenceFinding(options) {
2007
2031
  } : {}
2008
2032
  };
2009
2033
  }
2010
- function wholesaleCriterionAbstention(signal) {
2034
+ function wholesaleCriterionAbstention(signal, reasons) {
2035
+ const { insufficientDataReason } = classifyAbstentionReason(reasons);
2011
2036
  return {
2012
2037
  criterionId: signal.criterionId,
2013
2038
  positiveEvidence: [],
2014
2039
  findings: [],
2015
2040
  metrics: [],
2016
2041
  insufficientData: true,
2017
- notes: "Cejel abstained on this criterion because one or more relevant repository files could not be read. Resolve the content-read limitations and re-scan."
2042
+ ...insufficientDataReason !== void 0 ? { insufficientDataReason } : {},
2043
+ notes: insufficientDataReason !== void 0 ? "Cejel abstained on this criterion because relevant repository content was declined under the repository content size limit or excluded from scanning by policy. This is a disclosed coverage limit, not a read failure, and does not affect the composite score." : "Cejel abstained on this criterion because one or more relevant repository files could not be read. Resolve the content-read limitations and re-scan."
2018
2044
  };
2019
2045
  }
2046
+ function skipReasonsForCriterion(criterionId, tracked) {
2047
+ const combined = new Set(tracked.criterionSkipReasons.get(criterionId) ?? []);
2048
+ const prefix = contentReadSignalKey(criterionId, "");
2049
+ for (const [key, reasons] of tracked.signalSkipReasons) {
2050
+ if (!key.startsWith(prefix)) continue;
2051
+ for (const reason of reasons) combined.add(reason);
2052
+ }
2053
+ return combined;
2054
+ }
2055
+ function skipReasonsForSignal(criterionId, signalName, tracked) {
2056
+ const reasons = tracked.signalSkipReasons.get(contentReadSignalKey(criterionId, signalName));
2057
+ return reasons ? new Set(reasons) : void 0;
2058
+ }
2020
2059
  function buildWitanInputFromRepo(options) {
2021
2060
  const tracked = trackContentReads(options.repoPath, () => buildWitanInputFromRepoUntracked(options));
2022
2061
  const affectedCriteria = tracked.affectedCriteria;
2023
2062
  const rubricVersion = options.rubricVersion ?? WITAN_RUBRIC_VERSION;
2024
2063
  if (rubricVersion !== WITAN_RUBRIC_VERSION_V23) {
2025
- const signals2 = (tracked.value.signals ?? []).map(
2026
- (signal) => affectedCriteria.has(signal.criterionId) ? wholesaleCriterionAbstention(signal) : signal
2027
- );
2028
- const affectedCount = affectedCriteria.size;
2064
+ const readFailureCriteria = [];
2065
+ const coverageLimitedCriteria = [];
2066
+ const signals2 = (tracked.value.signals ?? []).map((signal) => {
2067
+ if (!affectedCriteria.has(signal.criterionId)) return signal;
2068
+ const reasons = skipReasonsForCriterion(signal.criterionId, tracked);
2069
+ const { insufficientDataReason } = classifyAbstentionReason(reasons);
2070
+ (insufficientDataReason !== void 0 ? coverageLimitedCriteria : readFailureCriteria).push(
2071
+ signal.criterionId
2072
+ );
2073
+ return wholesaleCriterionAbstention(signal, reasons);
2074
+ });
2029
2075
  const scanLimitations2 = [...tracked.value.scanLimitations ?? []];
2030
- if (affectedCount > 0 && scanLimitations2.length < 16) {
2076
+ if (readFailureCriteria.length > 0 && scanLimitations2.length < 16) {
2077
+ scanLimitations2.push(
2078
+ `${readFailureCriteria.length} ${readFailureCriteria.length === 1 ? "criterion" : "criteria"} abstained because relevant repository content could not be read. Counts and errno classes are recorded in contentReadSummary; file paths are intentionally omitted.`
2079
+ );
2080
+ }
2081
+ if (coverageLimitedCriteria.length > 0 && scanLimitations2.length < 16) {
2031
2082
  scanLimitations2.push(
2032
- `${affectedCount} ${affectedCount === 1 ? "criterion" : "criteria"} abstained because relevant repository content could not be read. Counts and errno classes are recorded in contentReadSummary; file paths are intentionally omitted.`
2083
+ `${coverageLimitedCriteria.length} ${coverageLimitedCriteria.length === 1 ? "criterion" : "criteria"} abstained because relevant repository content was declined under the repository content size limit or excluded from scanning by policy \u2014 a disclosed coverage limit, not a read failure. This does not affect the composite score.`
2033
2084
  );
2034
2085
  }
2035
2086
  return {
@@ -2039,45 +2090,68 @@ function buildWitanInputFromRepo(options) {
2039
2090
  contentReadSummary: tracked.summary
2040
2091
  };
2041
2092
  }
2042
- const wholesaleAbstainedCriteria = [];
2043
- const partiallyAbstainedSignals = [];
2093
+ const wholesaleAbstainedReadFailureCriteria = [];
2094
+ const wholesaleAbstainedCoverageLimitedCriteria = [];
2095
+ const partiallyAbstainedReadFailureSignals = [];
2096
+ const partiallyAbstainedCoverageLimitedSignals = [];
2044
2097
  const signals = (tracked.value.signals ?? []).map((signal) => {
2045
2098
  if (!affectedCriteria.has(signal.criterionId)) return signal;
2046
2099
  if (tracked.unattributedCriteria.has(signal.criterionId)) {
2047
- wholesaleAbstainedCriteria.push(signal.criterionId);
2048
- return wholesaleCriterionAbstention(signal);
2100
+ const reasons = tracked.criterionSkipReasons.get(signal.criterionId);
2101
+ const { insufficientDataReason: insufficientDataReason2 } = classifyAbstentionReason(reasons);
2102
+ (insufficientDataReason2 !== void 0 ? wholesaleAbstainedCoverageLimitedCriteria : wholesaleAbstainedReadFailureCriteria).push(signal.criterionId);
2103
+ return wholesaleCriterionAbstention(signal, reasons);
2049
2104
  }
2050
2105
  const prefix = contentReadSignalKey(signal.criterionId, "");
2051
2106
  const affectedNames = [...tracked.affectedSignals].filter((key) => key.startsWith(prefix)).map((key) => key.slice(prefix.length));
2052
2107
  if (affectedNames.length === 0) {
2053
- wholesaleAbstainedCriteria.push(signal.criterionId);
2108
+ wholesaleAbstainedReadFailureCriteria.push(signal.criterionId);
2054
2109
  return wholesaleCriterionAbstention(signal);
2055
2110
  }
2056
2111
  const affectedNameSet = new Set(affectedNames);
2057
2112
  const metrics = (signal.metrics ?? []).filter((metric2) => !affectedNameSet.has(metric2.name));
2058
2113
  const anythingSurvives = metrics.length > 0 || (signal.positiveEvidence?.length ?? 0) > 0 || (signal.findings?.length ?? 0) > 0;
2114
+ const droppedReasons = /* @__PURE__ */ new Set();
2115
+ for (const name of affectedNames) {
2116
+ for (const reason of skipReasonsForSignal(signal.criterionId, name, tracked) ?? []) {
2117
+ droppedReasons.add(reason);
2118
+ }
2119
+ }
2059
2120
  if (!anythingSurvives) {
2060
- wholesaleAbstainedCriteria.push(signal.criterionId);
2061
- return wholesaleCriterionAbstention(signal);
2121
+ const { insufficientDataReason: insufficientDataReason2 } = classifyAbstentionReason(droppedReasons);
2122
+ (insufficientDataReason2 !== void 0 ? wholesaleAbstainedCoverageLimitedCriteria : wholesaleAbstainedReadFailureCriteria).push(signal.criterionId);
2123
+ return wholesaleCriterionAbstention(signal, droppedReasons);
2062
2124
  }
2125
+ const { insufficientDataReason } = classifyAbstentionReason(droppedReasons);
2126
+ const isReadFailure = insufficientDataReason === void 0;
2063
2127
  for (const name of affectedNames) {
2064
- partiallyAbstainedSignals.push(`${signal.criterionId}.${name}`);
2128
+ (isReadFailure ? partiallyAbstainedReadFailureSignals : partiallyAbstainedCoverageLimitedSignals).push(`${signal.criterionId}.${name}`);
2065
2129
  }
2066
2130
  return {
2067
2131
  ...signal,
2068
2132
  metrics,
2069
- notes: `Cejel abstained on ${[...affectedNames].sort().join(", ")} in this criterion because that content could not be read; other signals in this criterion were computed from readable files and are unaffected.`
2133
+ notes: isReadFailure ? `Cejel abstained on ${[...affectedNames].sort().join(", ")} in this criterion because that content could not be read; other signals in this criterion were computed from readable files and are unaffected.` : `Cejel declined ${[...affectedNames].sort().join(", ")} in this criterion under the repository content size limit or an extension exclusion; other signals in this criterion were computed from readable files and are unaffected. This is a disclosed coverage limit, not a read failure.`
2070
2134
  };
2071
2135
  });
2072
2136
  const scanLimitations = [...tracked.value.scanLimitations ?? []];
2073
- if (wholesaleAbstainedCriteria.length > 0 && scanLimitations.length < 16) {
2137
+ if (wholesaleAbstainedReadFailureCriteria.length > 0 && scanLimitations.length < 16) {
2138
+ scanLimitations.push(
2139
+ `${wholesaleAbstainedReadFailureCriteria.length} ${wholesaleAbstainedReadFailureCriteria.length === 1 ? "criterion" : "criteria"} abstained because relevant repository content could not be read and the affected signal could not be isolated. Counts and errno classes are recorded in contentReadSummary; file paths are intentionally omitted.`
2140
+ );
2141
+ }
2142
+ if (wholesaleAbstainedCoverageLimitedCriteria.length > 0 && scanLimitations.length < 16) {
2143
+ scanLimitations.push(
2144
+ `${wholesaleAbstainedCoverageLimitedCriteria.length} ${wholesaleAbstainedCoverageLimitedCriteria.length === 1 ? "criterion" : "criteria"} abstained because relevant repository content was declined under the repository content size limit or excluded from scanning by policy, and the affected signal could not be isolated. This is a disclosed coverage limit, not a read failure, and does not affect the composite score.`
2145
+ );
2146
+ }
2147
+ if (partiallyAbstainedReadFailureSignals.length > 0 && scanLimitations.length < 16) {
2074
2148
  scanLimitations.push(
2075
- `${wholesaleAbstainedCriteria.length} ${wholesaleAbstainedCriteria.length === 1 ? "criterion" : "criteria"} abstained because relevant repository content could not be read and the affected signal could not be isolated. Counts and errno classes are recorded in contentReadSummary; file paths are intentionally omitted.`
2149
+ `${partiallyAbstainedReadFailureSignals.length} ${partiallyAbstainedReadFailureSignals.length === 1 ? "signal" : "signals"} abstained because its own repository content could not be read, while other signals in the same criterion were unaffected: ${[...partiallyAbstainedReadFailureSignals].sort().join(", ")}.`
2076
2150
  );
2077
2151
  }
2078
- if (partiallyAbstainedSignals.length > 0 && scanLimitations.length < 16) {
2152
+ if (partiallyAbstainedCoverageLimitedSignals.length > 0 && scanLimitations.length < 16) {
2079
2153
  scanLimitations.push(
2080
- `${partiallyAbstainedSignals.length} ${partiallyAbstainedSignals.length === 1 ? "signal" : "signals"} abstained because its own repository content could not be read, while other signals in the same criterion were unaffected: ${[...partiallyAbstainedSignals].sort().join(", ")}.`
2154
+ `${partiallyAbstainedCoverageLimitedSignals.length} ${partiallyAbstainedCoverageLimitedSignals.length === 1 ? "signal" : "signals"} declined under the repository content size limit or an extension exclusion, while other signals in the same criterion were unaffected: ${[...partiallyAbstainedCoverageLimitedSignals].sort().join(", ")}. This is a disclosed coverage limit, not a read failure.`
2081
2155
  );
2082
2156
  }
2083
2157
  return {
@@ -2107,6 +2181,7 @@ function buildWitanInputFromRepoUntracked(options) {
2107
2181
  const usesV21ExecutedEscalations = rubricVersion === WITAN_RUBRIC_VERSION_V21 || rubricVersion === WITAN_RUBRIC_VERSION_V22 || rubricVersion === WITAN_RUBRIC_VERSION_V23;
2108
2182
  const usesV22PackageStartEntrypoint = rubricVersion === WITAN_RUBRIC_VERSION_V22 || rubricVersion === WITAN_RUBRIC_VERSION_V23;
2109
2183
  const usesV23CommandCoverage = rubricVersion === WITAN_RUBRIC_VERSION_V23;
2184
+ const usesV23PemPrivateKeyGrammar = rubricVersion === WITAN_RUBRIC_VERSION_V23;
2110
2185
  const structuralArchetype = classifyRepoArchetype(inventoryFiles, rubricVersion);
2111
2186
  const readableArchetype = rubricVersion === WITAN_RUBRIC_VERSION_V13 || rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15 || rubricVersion === WITAN_RUBRIC_VERSION_V16 || usesV17DetectorClosure(rubricVersion) ? applyReadableSourceRepresentationGate(
2112
2187
  options.repoPath,
@@ -2143,6 +2218,7 @@ function buildWitanInputFromRepoUntracked(options) {
2143
2218
  usesV21ExecutedEscalations,
2144
2219
  usesV22PackageStartEntrypoint,
2145
2220
  usesV23CommandCoverage,
2221
+ usesV23PemPrivateKeyGrammar,
2146
2222
  reviewableSourceProof,
2147
2223
  inventoryAbsenceContext,
2148
2224
  scanLimitations
@@ -2754,7 +2830,7 @@ function buildNotApplicableSignal(criterionId, reason) {
2754
2830
  notApplicable: true
2755
2831
  };
2756
2832
  }
2757
- function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV18NativeRls, useV19CommitYear, useV20A3ExplicitGaps, useV21ExecutedEscalations, useV22PackageStartEntrypoint, useV23CommandCoverage, reviewableSourceProof, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT, scanLimitations = /* @__PURE__ */ new Set()) {
2833
+ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV18NativeRls, useV19CommitYear, useV20A3ExplicitGaps, useV21ExecutedEscalations, useV22PackageStartEntrypoint, useV23CommandCoverage, useV23PemPrivateKeyGrammar, reviewableSourceProof, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT, scanLimitations = /* @__PURE__ */ new Set()) {
2758
2834
  const signals = [];
2759
2835
  const mono = resolveMonorepoContext(repoPath, scanLimitations);
2760
2836
  const collectCriterion = (criterionId, collect) => withContentReadCriterion(criterionId, collect);
@@ -2781,6 +2857,7 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
2781
2857
  useV36Detectors,
2782
2858
  useV39Detectors,
2783
2859
  useV47Detectors,
2860
+ useV23PemPrivateKeyGrammar,
2784
2861
  useV18NativeRls,
2785
2862
  inventoryAbsenceContext,
2786
2863
  scanLimitations
@@ -3196,7 +3273,7 @@ function buildA1AuthenticatedAbsenceSignal(repoPath, proof, inventoryCount, inve
3196
3273
  notes: `V17 authenticated absence: ${proof.eligibleSourceFileCount} criterion-ratable of ${proof.sourceShapedFileCount} source-shaped files; sampled ${proof.sampledFileCount}; ${proof.readableSampledFileCount} representative-text files.`
3197
3274
  };
3198
3275
  }
3199
- function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV18NativeRls, inventoryAbsenceContext, scanLimitations) {
3276
+ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV23PemPrivateKeyGrammar, useV18NativeRls, inventoryAbsenceContext, scanLimitations) {
3200
3277
  const evidence = [];
3201
3278
  const findings = [];
3202
3279
  const gitignore = repoFiles.find((file) => basename(file) === ".gitignore");
@@ -3219,7 +3296,8 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
3219
3296
  path,
3220
3297
  useV36Detectors,
3221
3298
  useV39Detectors,
3222
- useV47Detectors
3299
+ useV47Detectors,
3300
+ useV23PemPrivateKeyGrammar
3223
3301
  );
3224
3302
  if (!found) continue;
3225
3303
  match_ = { path, match: found };
@@ -3227,7 +3305,9 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
3227
3305
  }
3228
3306
  const fingerprintsByPath = /* @__PURE__ */ new Map();
3229
3307
  if (useV27Detectors) {
3230
- for (const path of repoFiles.filter((candidate) => isCredentialHistoryPath(candidate))) {
3308
+ for (const path of repoFiles.filter(
3309
+ (candidate) => isCredentialHistoryPath(candidate, useV36Detectors, useV23PemPrivateKeyGrammar)
3310
+ )) {
3231
3311
  fingerprintsByPath.set(
3232
3312
  path,
3233
3313
  new Set(
@@ -3236,7 +3316,8 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
3236
3316
  path,
3237
3317
  useV36Detectors,
3238
3318
  useV39Detectors,
3239
- useV47Detectors
3319
+ useV47Detectors,
3320
+ useV23PemPrivateKeyGrammar
3240
3321
  )
3241
3322
  )
3242
3323
  );
@@ -3253,7 +3334,8 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
3253
3334
  useV33Detectors,
3254
3335
  useV36Detectors,
3255
3336
  useV39Detectors,
3256
- useV47Detectors
3337
+ useV47Detectors,
3338
+ useV23PemPrivateKeyGrammar
3257
3339
  );
3258
3340
  const hasConfirmedSecretFinding = committedSecret != null || historySecretScan?.evidence != null;
3259
3341
  const cryptoHygiene = withContentReadSignal(
@@ -3380,15 +3462,16 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
3380
3462
  if (committedSecret) {
3381
3463
  const isTestPath = isTestOrFixturePath(committedSecret.path);
3382
3464
  const isDefaultAdmin = committedSecret.match.kind === "default_admin";
3465
+ const isPemPrivateKey = committedSecret.match.kind === "pem_private_key";
3383
3466
  findings.push({
3384
3467
  severity: isTestPath ? "info" : "critical",
3385
- summary: isDefaultAdmin ? isTestPath ? `Default administrative credential in a test/fixture file (${committedSecret.path}) \u2014 likely fixture data, not a production deployment default; verify.` : "An explicit default administrative credential appears committed in production configuration." : isTestPath ? `Secret-shaped value in a test/fixture file (${committedSecret.path}) \u2014 likely fixture data, not a production leak; verify.` : "Secret-shaped value appears committed in the scanned repository.",
3468
+ summary: isDefaultAdmin ? isTestPath ? `Default administrative credential in a test/fixture file (${committedSecret.path}) \u2014 likely fixture data, not a production deployment default; verify.` : "An explicit default administrative credential appears committed in production configuration." : isPemPrivateKey ? isTestPath ? `PEM-formatted private key in a test/fixture file (${committedSecret.path}) \u2014 likely fixture data, not a production leak; verify.` : "A PEM-formatted private key appears committed in the scanned repository." : isTestPath ? `Secret-shaped value in a test/fixture file (${committedSecret.path}) \u2014 likely fixture data, not a production leak; verify.` : "Secret-shaped value appears committed in the scanned repository.",
3386
3469
  evidence: evidenceForRelativeAtLine(
3387
3470
  repoPath,
3388
3471
  committedSecret.path,
3389
3472
  "secret_scan",
3390
3473
  secretEvidenceLabel(
3391
- isDefaultAdmin ? "Committed default administrative credential" : "Committed secret-shaped value",
3474
+ isDefaultAdmin ? "Committed default administrative credential" : isPemPrivateKey ? "Committed PEM-formatted private key" : "Committed secret-shaped value",
3392
3475
  committedSecret.match
3393
3476
  ),
3394
3477
  committedSecret.match.line
@@ -3419,9 +3502,10 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
3419
3502
  if (historySecretScan?.evidence) {
3420
3503
  const historyPath = historySecretScan.evidence.path ?? "";
3421
3504
  const isTestPath = isTestOrFixturePath(historyPath);
3505
+ const isPemPrivateKey = historySecretScan.secretKind === "pem_private_key";
3422
3506
  findings.push({
3423
3507
  severity: isTestPath ? "info" : "critical",
3424
- summary: isTestPath ? `Secret-shaped value in git history for a test/fixture file (${historyPath}) \u2014 likely fixture data, not a production leak; verify.` : "Secret-shaped value appears in recent git history.",
3508
+ summary: isPemPrivateKey ? isTestPath ? `PEM-formatted private key in git history for a test/fixture file (${historyPath}) \u2014 likely fixture data, not a production leak; verify.` : "A PEM-formatted private key appears in recent git history." : isTestPath ? `Secret-shaped value in git history for a test/fixture file (${historyPath}) \u2014 likely fixture data, not a production leak; verify.` : "Secret-shaped value appears in recent git history.",
3425
3509
  evidence: historySecretScan.evidence
3426
3510
  });
3427
3511
  } else if (historySecretScan?.truncated) {
@@ -4803,6 +4887,7 @@ var GATED_PRIVILEGE_CHECK_PATTERN = /pg_has_role\s*\(|has_role\s*\(|is not a mem
4803
4887
  var SET_ROLE_PATTERN = /\bset\s+(local\s+)?role\b/i;
4804
4888
  var REQUIRED_REVIEW_PATTERN = /required review|branch protection|protected branch/i;
4805
4889
  var HISTORY_SECRET_SCAN_CREDENTIAL_BLOB_LIMIT = 5e3;
4890
+ var V23_PEM_WIDENED_HISTORY_BLOB_LIMIT = 500;
4806
4891
  var SECRET_IDENTIFIER_KEYWORD_PATTERN = /secret|token|api[_-]?key|password|access[_-]?key/gi;
4807
4892
  var SINGLE_WHITESPACE_PATTERN = /\s/u;
4808
4893
  var SECRET_VALUE_BRANDED_PATTERN = /^(?:sk-[A-Za-z0-9_-]{24,}|sk-ant-api\d{2}-[A-Za-z0-9_-]{80,}|ghp_[A-Za-z0-9_]{30,}|xox[baprs]-[A-Za-z0-9-]{24,}|AKIA[0-9A-Z]{16}|vcp_[A-Za-z0-9_:-]{24,})$/;
@@ -4962,12 +5047,7 @@ function parseGitTrackedFiles(repoPath, output, includeHardExcluded) {
4962
5047
  continue;
4963
5048
  }
4964
5049
  if (stat.size > MAX_REPOSITORY_CONTENT_BYTES) {
4965
- const affectedCriteria = affectedCriteriaForUnavailablePath(file);
4966
- if (affectedCriteria.length > 0) {
4967
- recordContentSkipForCriteria(fullPath, "too_large", affectedCriteria, true);
4968
- } else {
4969
- recordContentSkip(fullPath, "too_large", true);
4970
- }
5050
+ recordContentSkip(fullPath, "too_large", true);
4971
5051
  continue;
4972
5052
  }
4973
5053
  } catch (error) {
@@ -4981,39 +5061,6 @@ function parseGitTrackedFiles(repoPath, output, includeHardExcluded) {
4981
5061
  }
4982
5062
  return files.sort();
4983
5063
  }
4984
- function affectedCriteriaForUnavailablePath(file) {
4985
- const affected = /* @__PURE__ */ new Set();
4986
- if (isDependencyManifest(file) || isLockfile(file)) affected.add("A4");
4987
- if (isCiWorkflow(file)) {
4988
- affected.add("A1");
4989
- affected.add("A3");
4990
- affected.add("B2");
4991
- affected.add("B3");
4992
- }
4993
- if (isDeployConfig(file)) {
4994
- affected.add("A3");
4995
- affected.add("B2");
4996
- }
4997
- if (isTestFile(file)) {
4998
- affected.add("A1");
4999
- affected.add("A2");
5000
- }
5001
- if (isImplementationFile(file)) {
5002
- affected.add("A1");
5003
- affected.add("A2");
5004
- affected.add("A3");
5005
- affected.add("B1");
5006
- affected.add("B6");
5007
- }
5008
- if (/\.(?:md|mdx|rst|txt)$/i.test(file)) {
5009
- affected.add("A5");
5010
- affected.add("B4");
5011
- affected.add("B5");
5012
- affected.add("B6");
5013
- }
5014
- if (isAuditFile(file)) affected.add("B5");
5015
- return [...affected];
5016
- }
5017
5064
  function directoryInventory(repoPath) {
5018
5065
  const files = [];
5019
5066
  const rootDevice = statSync(repoPath).dev;
@@ -5135,12 +5182,7 @@ function visitRepoDir(repoPath, dirPath, files, rootDevice) {
5135
5182
  }
5136
5183
  const repoRelativePath = relative(repoPath, fullPath);
5137
5184
  if (size > MAX_REPOSITORY_CONTENT_BYTES) {
5138
- const affectedCriteria = affectedCriteriaForUnavailablePath(repoRelativePath);
5139
- if (affectedCriteria.length > 0) {
5140
- recordContentSkipForCriteria(fullPath, "too_large", affectedCriteria);
5141
- } else {
5142
- recordContentSkip(fullPath, "too_large");
5143
- }
5185
+ recordContentSkip(fullPath, "too_large");
5144
5186
  continue;
5145
5187
  }
5146
5188
  if (!isPotentialContentPath(repoRelativePath)) {
@@ -7066,7 +7108,69 @@ function fileContains(repoPath, file, pattern) {
7066
7108
  if (!isRegularFile(fullPath)) return false;
7067
7109
  return pattern.test(readRepoText(fullPath, "utf8"));
7068
7110
  }
7069
- function findCommittedSecretInFile(repoPath, file, useV36Detectors = false, useV39Detectors = false, useV47Detectors = false) {
7111
+ var PEM_PRIVATE_KEY_ASSIGNMENT_PATTERN = /(?:["'])?([A-Za-z_$][\w$.-]*)(?:["'])?\s*[:=]\s*(?:\|[-+0-9]*\s*\r?\n[ \t]*)?["'`]?-----BEGIN((?: (?:RSA|DSA|EC|OPENSSH|ENCRYPTED))? PRIVATE KEY)-----([\s\S]{0,32768}?)-----END\2-----/g;
7112
+ var BARE_PEM_PRIVATE_KEY_PATTERN = /["'`]?-----BEGIN((?: (?:RSA|DSA|EC|OPENSSH|ENCRYPTED))? PRIVATE KEY)-----([\s\S]{0,32768}?)-----END\1-----/g;
7113
+ var BARE_PEM_KEY_FILE_PATTERN = /(?:^|\/)id_(?:rsa|dsa|ecdsa|ed25519)$|\.(?:pem|key)$/i;
7114
+ function isBarePemKeyFile(file) {
7115
+ return BARE_PEM_KEY_FILE_PATTERN.test(file);
7116
+ }
7117
+ var DEV_OR_SELF_SIGNED_KEY_CONTEXT_PATTERN = /(?<![A-Za-z0-9])(?:dev|development|local|localhost|self[-_]?signed|snakeoil|insecure|dummy|sample|demo|placeholder|fixture|generated)(?![A-Za-z0-9])/i;
7118
+ var ROTATED_OR_REVOKED_KEY_CONTEXT_PATTERN = /(?<![A-Za-z0-9])(?:rotated|revoked|expired|deprecated|compromised|invalidated|superseded)(?![A-Za-z0-9])|do[-_ ]not[-_ ]use|no longer valid/i;
7119
+ function isPlausiblePemKeyBody(rawBody) {
7120
+ const normalized = rawBody.replace(/\\[nr]|[\r\n\s]/g, "");
7121
+ if (normalized.length < 40) return false;
7122
+ if (!/^[A-Za-z0-9+/=]+$/.test(normalized)) return false;
7123
+ return !isPlaceholderSecretValue(normalized);
7124
+ }
7125
+ function scanForPemPrivateKeyMatch(pattern, contents, lines, hasIdentifierGroup) {
7126
+ pattern.lastIndex = 0;
7127
+ let match;
7128
+ while ((match = pattern.exec(contents)) !== null) {
7129
+ const full = match[0];
7130
+ const identifier = hasIdentifierGroup ? match[1] ?? "" : "";
7131
+ const body = (hasIdentifierGroup ? match[3] : match[2]) ?? "";
7132
+ const line = contents.slice(0, match.index).split("\n").length;
7133
+ const contextLines = lines.slice(Math.max(0, line - 5), line + 4).join("\n");
7134
+ const isDevOrSelfSigned = DEV_OR_SELF_SIGNED_KEY_CONTEXT_PATTERN.test(
7135
+ `${identifier}
7136
+ ${contextLines}`
7137
+ );
7138
+ const isRotatedOrRevoked = ROTATED_OR_REVOKED_KEY_CONTEXT_PATTERN.test(contextLines);
7139
+ if (isPlausiblePemKeyBody(body) && !isDevOrSelfSigned && !isRotatedOrRevoked) {
7140
+ const value = full.slice(full.indexOf("-----BEGIN"));
7141
+ const characterClasses = [
7142
+ /[a-z]/.test(value) ? "lower" : "",
7143
+ /[A-Z]/.test(value) ? "upper" : "",
7144
+ /\d/.test(value) ? "digit" : "",
7145
+ /[^A-Za-z0-9]/.test(value) ? "symbol" : ""
7146
+ ].filter(Boolean).join("+");
7147
+ return {
7148
+ identifier,
7149
+ line,
7150
+ valueLength: value.length,
7151
+ characterClasses: characterClasses || "other",
7152
+ valueFingerprint: createHash2("sha256").update(value).digest("hex"),
7153
+ kind: "pem_private_key"
7154
+ };
7155
+ }
7156
+ }
7157
+ return null;
7158
+ }
7159
+ function findPemPrivateKeyAssignment(contents, file) {
7160
+ if (!contents.includes("-----BEGIN")) return null;
7161
+ if (DEV_OR_SELF_SIGNED_KEY_CONTEXT_PATTERN.test(file)) return null;
7162
+ const lines = contents.split("\n");
7163
+ const assignmentMatch = scanForPemPrivateKeyMatch(
7164
+ PEM_PRIVATE_KEY_ASSIGNMENT_PATTERN,
7165
+ contents,
7166
+ lines,
7167
+ true
7168
+ );
7169
+ if (assignmentMatch) return assignmentMatch;
7170
+ if (!isBarePemKeyFile(file)) return null;
7171
+ return scanForPemPrivateKeyMatch(BARE_PEM_PRIVATE_KEY_PATTERN, contents, lines, false);
7172
+ }
7173
+ function findCommittedSecretInFile(repoPath, file, useV36Detectors = false, useV39Detectors = false, useV47Detectors = false, useV23PemPrivateKeyGrammar = false) {
7070
7174
  const fullPath = join(repoPath, file);
7071
7175
  if (!isRegularFile(fullPath)) return null;
7072
7176
  const contents = readRepoText(fullPath, "utf8");
@@ -7082,6 +7186,10 @@ function findCommittedSecretInFile(repoPath, file, useV36Detectors = false, useV
7082
7186
  allowCredentialNamedDigest: useV36Detectors
7083
7187
  });
7084
7188
  if (secret) return secret;
7189
+ if (useV23PemPrivateKeyGrammar) {
7190
+ const pemSecret = findPemPrivateKeyAssignment(secretScanContents, file);
7191
+ if (pemSecret) return pemSecret;
7192
+ }
7085
7193
  if (useV39Detectors && isEnvHistoryPath(file, useV36Detectors)) {
7086
7194
  const weakCredential = findWeakExplicitEnvCredential(secretScanContents);
7087
7195
  if (weakCredential) return weakCredential;
@@ -7174,7 +7282,7 @@ function hasTemplateOnlyEnvContent(repoPath, file) {
7174
7282
  return value.length === 0 || isPlaceholderSecretValue(value);
7175
7283
  });
7176
7284
  }
7177
- function findSecretFingerprintsInFile(repoPath, file, useV36Detectors = false, useV39Detectors = false, useV47Detectors = false) {
7285
+ function findSecretFingerprintsInFile(repoPath, file, useV36Detectors = false, useV39Detectors = false, useV47Detectors = false, useV23PemPrivateKeyGrammar = false) {
7178
7286
  const fullPath = join(repoPath, file);
7179
7287
  if (!isRegularFile(fullPath)) return [];
7180
7288
  const contents = readRepoText(fullPath, "utf8");
@@ -7191,6 +7299,10 @@ function findSecretFingerprintsInFile(repoPath, file, useV36Detectors = false, u
7191
7299
  Number.POSITIVE_INFINITY,
7192
7300
  { allowCredentialNamedDigest: useV36Detectors }
7193
7301
  ).map(({ valueFingerprint }) => valueFingerprint);
7302
+ if (useV23PemPrivateKeyGrammar) {
7303
+ const pemSecret = findPemPrivateKeyAssignment(scanContents, file);
7304
+ if (pemSecret) fingerprints.push(pemSecret.valueFingerprint);
7305
+ }
7194
7306
  if (useV39Detectors && isEnvHistoryPath(file, useV36Detectors)) {
7195
7307
  const weakCredential = findWeakExplicitEnvCredential(scanContents);
7196
7308
  if (weakCredential) fingerprints.push(weakCredential.valueFingerprint);
@@ -7334,24 +7446,32 @@ function declareHistoryFailure(scanLimitations, reason, operation, maxBufferByte
7334
7446
  `${describeGitFailure(reason, operation, maxBufferBytes)} Cejel continued with reduced Git history evidence.`
7335
7447
  );
7336
7448
  }
7337
- function collectHistorySecretEvidence(repoPath, excludeHead, currentSecretFingerprintsByPath, scanLimitations, authoredProductionOnly = false, useV36Detectors = false, useV39Detectors = false, useV47Detectors = false) {
7449
+ function collectHistorySecretEvidence(repoPath, excludeHead, currentSecretFingerprintsByPath, scanLimitations, authoredProductionOnly = false, useV36Detectors = false, useV39Detectors = false, useV47Detectors = false, useV23PemPrivateKeyGrammar = false) {
7338
7450
  const historyEntries = readCredentialHistoryEntries(
7339
7451
  repoPath,
7340
7452
  scanLimitations,
7341
- useV36Detectors
7453
+ useV36Detectors,
7454
+ useV23PemPrivateKeyGrammar
7342
7455
  );
7343
7456
  const head = excludeHead ? readGitHead(repoPath) : null;
7344
7457
  let scannedFiles = 0;
7458
+ let v23WidenedOnlyScanned = 0;
7345
7459
  let envPathEvidence = null;
7346
7460
  for (const entry of historyEntries) {
7347
7461
  if (head && entry.commit === head) continue;
7348
7462
  if (authoredProductionOnly && !(useV47Detectors ? isV47AuthoredProductionPath(entry.path) : useV39Detectors ? isV39AuthoredProductionPath(entry.path) : isAuthoredProductionPath(entry.path))) {
7349
7463
  continue;
7350
7464
  }
7351
- if (scannedFiles >= HISTORY_SECRET_SCAN_CREDENTIAL_BLOB_LIMIT) {
7352
- return { evidence: null, envPathEvidence, truncated: true };
7465
+ const isV23WidenedOnly = useV23PemPrivateKeyGrammar && isV23PemCredentialHistoryPath(historyPathBasename(entry.path.toLowerCase())) && !isCredentialHistoryPath(entry.path, useV36Detectors, false);
7466
+ if (isV23WidenedOnly) {
7467
+ if (v23WidenedOnlyScanned >= V23_PEM_WIDENED_HISTORY_BLOB_LIMIT) continue;
7468
+ v23WidenedOnlyScanned += 1;
7469
+ } else {
7470
+ if (scannedFiles >= HISTORY_SECRET_SCAN_CREDENTIAL_BLOB_LIMIT) {
7471
+ return { evidence: null, secretKind: null, envPathEvidence, truncated: true };
7472
+ }
7473
+ scannedFiles += 1;
7353
7474
  }
7354
- scannedFiles += 1;
7355
7475
  const contents = readGitBlob(repoPath, entry.commit, entry.path, scanLimitations);
7356
7476
  if (!contents) continue;
7357
7477
  const scanContents = prepareCredentialScanContents(
@@ -7365,6 +7485,12 @@ function collectHistorySecretEvidence(repoPath, excludeHead, currentSecretFinger
7365
7485
  let secretMatch = findRealSecretAssignment(scanContents, currentFingerprints, {
7366
7486
  allowCredentialNamedDigest: useV36Detectors
7367
7487
  });
7488
+ if (!secretMatch && useV23PemPrivateKeyGrammar) {
7489
+ const pemMatch = findPemPrivateKeyAssignment(scanContents, entry.path);
7490
+ if (pemMatch && !currentFingerprints.has(pemMatch.valueFingerprint)) {
7491
+ secretMatch = pemMatch;
7492
+ }
7493
+ }
7368
7494
  if (!secretMatch && useV39Detectors && isEnvHistoryPath(entry.path, useV36Detectors, useV47Detectors)) {
7369
7495
  const weakCredential = findWeakExplicitEnvCredential(scanContents);
7370
7496
  if (weakCredential && !currentFingerprints.has(weakCredential.valueFingerprint)) {
@@ -7375,11 +7501,15 @@ function collectHistorySecretEvidence(repoPath, excludeHead, currentSecretFinger
7375
7501
  return {
7376
7502
  evidence: {
7377
7503
  kind: "secret_scan",
7378
- label: secretEvidenceLabel("Committed secret in recent git history", secretMatch),
7504
+ label: secretEvidenceLabel(
7505
+ secretMatch.kind === "pem_private_key" ? "Committed PEM-formatted private key in recent git history" : "Committed secret in recent git history",
7506
+ secretMatch
7507
+ ),
7379
7508
  path: entry.path,
7380
7509
  line: secretMatch.line,
7381
7510
  contentHash: entry.commit
7382
7511
  },
7512
+ secretKind: secretMatch.kind ?? null,
7383
7513
  envPathEvidence,
7384
7514
  truncated: false
7385
7515
  };
@@ -7393,9 +7523,9 @@ function collectHistorySecretEvidence(repoPath, excludeHead, currentSecretFinger
7393
7523
  };
7394
7524
  }
7395
7525
  }
7396
- return { evidence: null, envPathEvidence, truncated: false };
7526
+ return { evidence: null, secretKind: null, envPathEvidence, truncated: false };
7397
7527
  }
7398
- function readCredentialHistoryEntries(repoPath, scanLimitations, useV36Detectors = false) {
7528
+ function readCredentialHistoryEntries(repoPath, scanLimitations, useV36Detectors = false, useV23PemPrivateKeyGrammar = false) {
7399
7529
  const entries = [];
7400
7530
  const seen = /* @__PURE__ */ new Set();
7401
7531
  const appendEntry = (entry) => {
@@ -7440,7 +7570,7 @@ function readCredentialHistoryEntries(repoPath, scanLimitations, useV36Detectors
7440
7570
  scanLimitations.add(CONTROL_CHARACTER_HISTORY_PATH_LIMITATION);
7441
7571
  continue;
7442
7572
  }
7443
- if (!isCredentialHistoryPath(path, useV36Detectors)) continue;
7573
+ if (!isCredentialHistoryPath(path, useV36Detectors, useV23PemPrivateKeyGrammar)) continue;
7444
7574
  appendEntry({ commit: currentCommit, path });
7445
7575
  }
7446
7576
  }
@@ -7448,17 +7578,19 @@ function readCredentialHistoryEntries(repoPath, scanLimitations, useV36Detectors
7448
7578
  for (const entry of readDeletedCredentialHistoryEntries(
7449
7579
  repoPath,
7450
7580
  scanLimitations,
7451
- useV36Detectors
7581
+ useV36Detectors,
7582
+ useV23PemPrivateKeyGrammar
7452
7583
  )) {
7453
7584
  appendEntry(entry);
7454
7585
  }
7455
7586
  return entries;
7456
7587
  }
7457
- function readDeletedCredentialHistoryEntries(repoPath, scanLimitations, useV36Detectors = false) {
7588
+ function readDeletedCredentialHistoryEntries(repoPath, scanLimitations, useV36Detectors = false, useV23PemPrivateKeyGrammar = false) {
7458
7589
  const deletedPaths = readDeletedCredentialHistoryPaths(
7459
7590
  repoPath,
7460
7591
  scanLimitations,
7461
- useV36Detectors
7592
+ useV36Detectors,
7593
+ useV23PemPrivateKeyGrammar
7462
7594
  );
7463
7595
  const entries = [];
7464
7596
  const seen = /* @__PURE__ */ new Set();
@@ -7472,7 +7604,7 @@ function readDeletedCredentialHistoryEntries(repoPath, scanLimitations, useV36De
7472
7604
  }
7473
7605
  return entries;
7474
7606
  }
7475
- function readDeletedCredentialHistoryPaths(repoPath, scanLimitations, useV36Detectors = false) {
7607
+ function readDeletedCredentialHistoryPaths(repoPath, scanLimitations, useV36Detectors = false, useV23PemPrivateKeyGrammar = false) {
7476
7608
  const historyResult = execGit(
7477
7609
  [
7478
7610
  "log",
@@ -7500,7 +7632,7 @@ function readDeletedCredentialHistoryPaths(repoPath, scanLimitations, useV36Dete
7500
7632
  continue;
7501
7633
  }
7502
7634
  if (!path || seen.has(path)) continue;
7503
- if (!isCredentialHistoryPath(path, useV36Detectors)) continue;
7635
+ if (!isCredentialHistoryPath(path, useV36Detectors, useV23PemPrivateKeyGrammar)) continue;
7504
7636
  seen.add(path);
7505
7637
  paths.push(path);
7506
7638
  }
@@ -7518,14 +7650,24 @@ function readHistoryCommitsForPath(repoPath, path, scanLimitations) {
7518
7650
  }
7519
7651
  return historyResult.stdout.split("\n").map((line) => line.trim()).filter((line) => /^[0-9a-f]{40}$/i.test(line));
7520
7652
  }
7521
- function isCredentialHistoryPath(path, _useV36Detectors = false) {
7653
+ var V23_KEY_SHAPED_JSON_PATH_PATTERN = /(?<![A-Za-z0-9])(?:service[-_]?account|private[-_]?key|creds?|key)(?![A-Za-z0-9])/i;
7654
+ function historyPathBasename(lowerPath) {
7655
+ return lowerPath.split("/").at(-1) ?? lowerPath;
7656
+ }
7657
+ function isV23PemCredentialHistoryPath(basename3) {
7658
+ return basename3.endsWith(".json") && V23_KEY_SHAPED_JSON_PATH_PATTERN.test(basename3);
7659
+ }
7660
+ function isCredentialHistoryPath(path, _useV36Detectors = false, useV23PemPrivateKeyGrammar = false) {
7522
7661
  if (isHardExcludedPath(path)) return false;
7523
7662
  const lowerPath = path.toLowerCase();
7524
- const basename3 = lowerPath.split("/").at(-1) ?? lowerPath;
7663
+ const basename3 = historyPathBasename(lowerPath);
7525
7664
  if (/\.env(?:\.|$)/.test(basename3)) {
7526
7665
  return true;
7527
7666
  }
7528
- return lowerPath.includes("secret") || lowerPath.includes("credential") || basename3.endsWith(".pem") || basename3.endsWith(".key") || basename3 === "id_rsa" || basename3.endsWith(".p12") || basename3.endsWith(".pfx");
7667
+ if (lowerPath.includes("secret") || lowerPath.includes("credential") || basename3.endsWith(".pem") || basename3.endsWith(".key") || basename3 === "id_rsa" || basename3.endsWith(".p12") || basename3.endsWith(".pfx")) {
7668
+ return true;
7669
+ }
7670
+ return useV23PemPrivateKeyGrammar && isV23PemCredentialHistoryPath(basename3);
7529
7671
  }
7530
7672
  function isEnvHistoryPath(path, useV36Detectors = false, useV47Detectors = false) {
7531
7673
  if (isEnvTemplatePath(path, useV47Detectors)) return false;
@@ -8300,11 +8442,11 @@ function usesV17DetectorClosure2(rubricVersion) {
8300
8442
  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 || rubricVersion === WITAN_RUBRIC_VERSION_V23;
8301
8443
  }
8302
8444
  var MAX_SIGNAL_ADJUSTMENT = 0.8;
8303
- function createWitanReport(input, inputSignals, rubric = WITAN_RUBRIC) {
8445
+ function createWitanReport(input, inputSignals, rubric = WITAN_RUBRIC, toolVersion) {
8304
8446
  const parsedInput = WitanReportInputSchema.parse(input);
8305
8447
  const signalsByCriterion = mergeSignalsByCriterion(parsedInput.signals);
8306
8448
  const readFailureAbstentions = new Set(
8307
- [...signalsByCriterion.values()].filter((signal) => signal.insufficientData === true).map((signal) => signal.criterionId)
8449
+ [...signalsByCriterion.values()].filter((signal) => signal.insufficientData === true && !isSelfImposedAbstention(signal)).map((signal) => signal.criterionId)
8308
8450
  );
8309
8451
  const hasInputSignals = inputSignals != null && inputSignals.length > 0;
8310
8452
  const inputSignalsByDimension = /* @__PURE__ */ new Map();
@@ -8409,7 +8551,8 @@ function createWitanReport(input, inputSignals, rubric = WITAN_RUBRIC) {
8409
8551
  // alter it (see classifyRepoArchetype in repo-signals.ts); per-criterion scoring above is
8410
8552
  // unaffected by archetype, only presentation layers (badge/terminal/verdict) key off it.
8411
8553
  ...parsedInput.archetype ? { archetype: parsedInput.archetype } : {},
8412
- ...insufficientSourceReason ? { insufficientSourceReason } : {}
8554
+ ...insufficientSourceReason ? { insufficientSourceReason } : {},
8555
+ ...toolVersion ? { toolVersion } : {}
8413
8556
  });
8414
8557
  }
8415
8558
  function applySignalAdjustment(nativeScore, dimension, signals) {
@@ -8479,6 +8622,14 @@ function statusAfterInputAdjustment(nativeStatus, nativeScore, adjustment, rubri
8479
8622
  );
8480
8623
  return (severity[adjustedSemanticStatus] ?? nativeSeverity) > nativeSeverity ? adjustedSemanticStatus : nativeStatus;
8481
8624
  }
8625
+ var SELF_IMPOSED_ABSTENTION_REASONS = /* @__PURE__ */ new Set([
8626
+ "too_large",
8627
+ "excluded_by_extension",
8628
+ "non_regular_file"
8629
+ ]);
8630
+ function isSelfImposedAbstention(signal) {
8631
+ return signal.insufficientDataReason !== void 0 && SELF_IMPOSED_ABSTENTION_REASONS.has(signal.insufficientDataReason);
8632
+ }
8482
8633
  function mergeSignalsByCriterion(signals) {
8483
8634
  const merged = /* @__PURE__ */ new Map();
8484
8635
  for (const signal of signals) {
@@ -8487,15 +8638,19 @@ function mergeSignalsByCriterion(signals) {
8487
8638
  merged.set(signal.criterionId, signal);
8488
8639
  continue;
8489
8640
  }
8641
+ const existingAbstains = existing.insufficientData === true;
8642
+ const signalAbstains = signal.insufficientData === true;
8643
+ const abstentionReason = !existingAbstains && !signalAbstains ? void 0 : [existing, signal].filter((candidate) => candidate.insufficientData === true).some((candidate) => !isSelfImposedAbstention(candidate)) ? void 0 : signal.insufficientDataReason ?? existing.insufficientDataReason;
8490
8644
  merged.set(signal.criterionId, {
8491
8645
  criterionId: signal.criterionId,
8492
8646
  positiveEvidence: [...existing.positiveEvidence, ...signal.positiveEvidence],
8493
8647
  findings: [...existing.findings, ...signal.findings],
8494
8648
  metrics: [...existing.metrics ?? [], ...signal.metrics ?? []],
8495
8649
  notes: signal.notes ?? existing.notes,
8496
- // Read-failure abstention wins on merge: partial sibling evidence cannot turn an
8497
- // incomplete criterion into a score. Otherwise N/A retains its historical precedence.
8498
- ...existing.insufficientData === true || signal.insufficientData === true ? { insufficientData: true } : existing.notApplicable === true || signal.notApplicable === true ? { notApplicable: true } : {}
8650
+ ...existingAbstains || signalAbstains ? {
8651
+ insufficientData: true,
8652
+ ...abstentionReason !== void 0 ? { insufficientDataReason: abstentionReason } : {}
8653
+ } : existing.notApplicable === true || signal.notApplicable === true ? { notApplicable: true } : {}
8499
8654
  });
8500
8655
  }
8501
8656
  return merged;
@@ -8931,7 +9086,12 @@ function scoreRepoWithPublicCejel(options) {
8931
9086
  ...options.rubricVersion ? { rubricVersion: options.rubricVersion } : {}
8932
9087
  });
8933
9088
  const inputSignals = resolvePublicIngestSignals(options);
8934
- const report = createWitanReport(input, inputSignals.length > 0 ? inputSignals : void 0);
9089
+ const report = createWitanReport(
9090
+ input,
9091
+ inputSignals.length > 0 ? inputSignals : void 0,
9092
+ void 0,
9093
+ options.toolVersion
9094
+ );
8935
9095
  if (report.verdict !== "insufficient_source" || !isWitanNoMeasurementAbstention(report)) {
8936
9096
  return report;
8937
9097
  }
@@ -9189,7 +9349,8 @@ function runCejelScan(options) {
9189
9349
  // the repository. Direct public/leaderboard scans default this off because their targets
9190
9350
  // are third-party repositories and must opt in explicitly.
9191
9351
  autoDiscoverIngest: true,
9192
- warnOnEmptyIngestMatch: options.warnOnEmptyIngestMatch
9352
+ warnOnEmptyIngestMatch: options.warnOnEmptyIngestMatch,
9353
+ ...options.toolVersion ? { toolVersion: options.toolVersion } : {}
9193
9354
  });
9194
9355
  const summary = buildWitanCliSummary(report);
9195
9356
  return { report, summary, generatedAt };
@@ -5,10 +5,10 @@ import {
5
5
  describeGitFailure,
6
6
  execGit,
7
7
  isExpectedGitAbsence
8
- } from "./chunk-OIGCYULH.js";
8
+ } from "./chunk-KNQBI3IX.js";
9
9
  import {
10
10
  init_esm_shims
11
- } from "./chunk-ZNVILRHK.js";
11
+ } from "./chunk-V3B2V4WA.js";
12
12
 
13
13
  // src/packs/llm/files.ts
14
14
  init_esm_shims();
@@ -4,7 +4,7 @@ const require = __cejelCreateRequire(import.meta.url);
4
4
  import {
5
5
  __export,
6
6
  init_esm_shims
7
- } from "./chunk-ZNVILRHK.js";
7
+ } from "./chunk-V3B2V4WA.js";
8
8
 
9
9
  // node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
10
10
  var external_exports = {};
@@ -3,7 +3,7 @@ import { createRequire as __cejelCreateRequire } from 'node:module';
3
3
  const require = __cejelCreateRequire(import.meta.url);
4
4
  import {
5
5
  init_esm_shims
6
- } from "./chunk-ZNVILRHK.js";
6
+ } from "./chunk-V3B2V4WA.js";
7
7
 
8
8
  // src/witan/git-exec.ts
9
9
  init_esm_shims();
@@ -62,10 +62,35 @@ var MAX_REPOSITORY_CONTENT_BYTES = 512e3;
62
62
  init_esm_shims();
63
63
  import { closeSync, lstatSync, openSync, readFileSync, readSync } from "fs";
64
64
  import { resolve } from "path";
65
+ var READ_FAILURE_REASONS = /* @__PURE__ */ new Set(["unreadable", "denied_path"]);
66
+ function isReadFailureReason(reason) {
67
+ return READ_FAILURE_REASONS.has(reason);
68
+ }
69
+ function classifyAbstentionReason(reasons) {
70
+ if (!reasons || reasons.size === 0) return {};
71
+ for (const reason of reasons) {
72
+ if (isReadFailureReason(reason)) return {};
73
+ }
74
+ const priority = [
75
+ "too_large",
76
+ "excluded_by_extension",
77
+ "non_regular_file"
78
+ ];
79
+ const chosen = priority.find((reason) => reasons.has(reason));
80
+ return chosen ? { insufficientDataReason: chosen } : {};
81
+ }
65
82
  function contentReadSignalKey(criterionId, signalId) {
66
83
  return [criterionId, signalId].join(":");
67
84
  }
68
85
  var activeSession;
86
+ function addSkipReason(map, key, reason) {
87
+ const existing = map.get(key);
88
+ if (existing) {
89
+ existing.add(reason);
90
+ } else {
91
+ map.set(key, /* @__PURE__ */ new Set([reason]));
92
+ }
93
+ }
69
94
  function errnoClass(error) {
70
95
  if (typeof error !== "object" || error === null || !("code" in error)) return null;
71
96
  const code = error.code;
@@ -79,10 +104,13 @@ function recordSkip(path2, reason, errno, affectsCurrentCriterion = false, dedup
79
104
  const signals = session.signals;
80
105
  if (signals && signals.length > 0) {
81
106
  for (const signal of signals) {
82
- session.affectedSignals.add(contentReadSignalKey(session.criterion, signal));
107
+ const key = contentReadSignalKey(session.criterion, signal);
108
+ session.affectedSignals.add(key);
109
+ addSkipReason(session.signalSkipReasons, key, reason);
83
110
  }
84
111
  } else {
85
112
  session.unattributedCriteria.add(session.criterion);
113
+ addSkipReason(session.criterionSkipReasons, session.criterion, reason);
86
114
  }
87
115
  }
88
116
  if (deduplicate) {
@@ -121,7 +149,9 @@ function trackContentReads(_repoPath, collect) {
121
149
  summary: summaryFor(existing),
122
150
  affectedCriteria: existing.affectedCriteria,
123
151
  affectedSignals: existing.affectedSignals,
124
- unattributedCriteria: existing.unattributedCriteria
152
+ unattributedCriteria: existing.unattributedCriteria,
153
+ criterionSkipReasons: existing.criterionSkipReasons,
154
+ signalSkipReasons: existing.signalSkipReasons
125
155
  };
126
156
  }
127
157
  const session = {
@@ -136,7 +166,9 @@ function trackContentReads(_repoPath, collect) {
136
166
  deduplicatedSkips: /* @__PURE__ */ new Set(),
137
167
  affectedCriteria: /* @__PURE__ */ new Set(),
138
168
  affectedSignals: /* @__PURE__ */ new Set(),
139
- unattributedCriteria: /* @__PURE__ */ new Set()
169
+ unattributedCriteria: /* @__PURE__ */ new Set(),
170
+ criterionSkipReasons: /* @__PURE__ */ new Map(),
171
+ signalSkipReasons: /* @__PURE__ */ new Map()
140
172
  };
141
173
  activeSession = session;
142
174
  try {
@@ -146,7 +178,9 @@ function trackContentReads(_repoPath, collect) {
146
178
  summary: summaryFor(session),
147
179
  affectedCriteria: session.affectedCriteria,
148
180
  affectedSignals: session.affectedSignals,
149
- unattributedCriteria: session.unattributedCriteria
181
+ unattributedCriteria: session.unattributedCriteria,
182
+ criterionSkipReasons: session.criterionSkipReasons,
183
+ signalSkipReasons: session.signalSkipReasons
150
184
  };
151
185
  } finally {
152
186
  activeSession = void 0;
@@ -183,16 +217,6 @@ function withContentReadSignals(criterionId, signalIds, collect) {
183
217
  function recordContentSkip(path2, reason, deduplicate = false) {
184
218
  recordSkip(path2, reason, void 0, false, deduplicate);
185
219
  }
186
- function recordContentSkipForCriteria(path2, reason, criteria, deduplicate = false) {
187
- const session = activeSession;
188
- if (session) {
189
- for (const criterion of criteria) {
190
- session.affectedCriteria.add(criterion);
191
- session.unattributedCriteria.add(criterion);
192
- }
193
- }
194
- recordSkip(path2, reason, void 0, false, deduplicate);
195
- }
196
220
  function recordFilesystemSkip(path2, error, deniedContext = false, deduplicate = true) {
197
221
  const errno = errnoClass(error);
198
222
  if (!errno) throw error;
@@ -264,13 +288,13 @@ export {
264
288
  __filename,
265
289
  init_esm_shims,
266
290
  MAX_REPOSITORY_CONTENT_BYTES,
291
+ classifyAbstentionReason,
267
292
  contentReadSignalKey,
268
293
  trackContentReads,
269
294
  withContentReadCriterion,
270
295
  withContentReadSignal,
271
296
  withContentReadSignals,
272
297
  recordContentSkip,
273
- recordContentSkipForCriteria,
274
298
  recordFilesystemSkip,
275
299
  readRepoText,
276
300
  readRepoTextPrefix
@@ -7,7 +7,7 @@ import {
7
7
  __filename,
8
8
  __require,
9
9
  init_esm_shims
10
- } from "./chunk-ZNVILRHK.js";
10
+ } from "./chunk-V3B2V4WA.js";
11
11
 
12
12
  // node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/typescript.js
13
13
  var require_typescript = __commonJS({
package/dist/index.js CHANGED
@@ -21,12 +21,12 @@ import {
21
21
  runCejelScan,
22
22
  serializeWitanReport,
23
23
  verifyWitanAttestationBinding
24
- } from "./chunk-QTLLASOO.js";
25
- import "./chunk-OIGCYULH.js";
26
- import "./chunk-UYJBSQCV.js";
24
+ } from "./chunk-6R3VJGJ2.js";
25
+ import "./chunk-KNQBI3IX.js";
26
+ import "./chunk-J6R5OHN7.js";
27
27
  import {
28
28
  init_esm_shims
29
- } from "./chunk-ZNVILRHK.js";
29
+ } from "./chunk-V3B2V4WA.js";
30
30
 
31
31
  // src/index.ts
32
32
  init_esm_shims();
@@ -324,7 +324,8 @@ async function runWitanCli(args, rubricVersion) {
324
324
  ...options.productDisplayName ? { productDisplayName: options.productDisplayName } : {},
325
325
  ...effectiveRubricVersion ? { rubricVersion: effectiveRubricVersion } : {},
326
326
  ingestPatterns: options.ingestPatterns,
327
- warnOnEmptyIngestMatch: !options.quiet
327
+ warnOnEmptyIngestMatch: !options.quiet,
328
+ toolVersion: cliVersion()
328
329
  });
329
330
  const attestation = createWitanAttestation(report, {
330
331
  toolVersion: cliVersion(),
package/dist/mcp/index.js CHANGED
@@ -5,20 +5,20 @@ import {
5
5
  renderWitanBadgeSvg,
6
6
  renderWitanHtmlReport,
7
7
  runCejelScan
8
- } from "../chunk-QTLLASOO.js";
9
- import "../chunk-OIGCYULH.js";
8
+ } from "../chunk-6R3VJGJ2.js";
9
+ import "../chunk-KNQBI3IX.js";
10
10
  import {
11
11
  ZodFirstPartyTypeKind,
12
12
  ZodOptional,
13
13
  external_exports,
14
14
  objectType
15
- } from "../chunk-UYJBSQCV.js";
15
+ } from "../chunk-J6R5OHN7.js";
16
16
  import {
17
17
  __commonJS,
18
18
  __export,
19
19
  __toESM,
20
20
  init_esm_shims
21
- } from "../chunk-ZNVILRHK.js";
21
+ } from "../chunk-V3B2V4WA.js";
22
22
 
23
23
  // node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/compile/codegen/code.js
24
24
  var require_code = __commonJS({
@@ -17280,7 +17280,7 @@ function createCejelMcpServer(identity) {
17280
17280
  async ({ path, format }) => {
17281
17281
  let result;
17282
17282
  try {
17283
- result = runCejelScan({ repoPath: resolve(path) });
17283
+ result = runCejelScan({ repoPath: resolve(path), toolVersion: identity.version });
17284
17284
  } catch (error2) {
17285
17285
  const message = error2 instanceof Error ? error2.message : String(error2);
17286
17286
  return {
@@ -3,18 +3,18 @@ import { createRequire as __cejelCreateRequire } from 'node:module';
3
3
  const require = __cejelCreateRequire(import.meta.url);
4
4
  import {
5
5
  listCejelLlmPackFiles
6
- } from "../../chunk-EP7YYWEP.js";
6
+ } from "../../chunk-ERNYMFH4.js";
7
7
  import {
8
8
  execGit
9
- } from "../../chunk-OIGCYULH.js";
9
+ } from "../../chunk-KNQBI3IX.js";
10
10
  import {
11
11
  require_typescript
12
- } from "../../chunk-XHAETB7N.js";
12
+ } from "../../chunk-ZPQOZLB7.js";
13
13
  import {
14
14
  __toESM,
15
15
  init_esm_shims,
16
16
  readRepoText
17
- } from "../../chunk-ZNVILRHK.js";
17
+ } from "../../chunk-V3B2V4WA.js";
18
18
 
19
19
  // src/packs/d-series/index.ts
20
20
  init_esm_shims();
@@ -3,15 +3,15 @@ import { createRequire as __cejelCreateRequire } from 'node:module';
3
3
  const require = __cejelCreateRequire(import.meta.url);
4
4
  import {
5
5
  external_exports
6
- } from "../../chunk-UYJBSQCV.js";
6
+ } from "../../chunk-J6R5OHN7.js";
7
7
  import {
8
8
  require_typescript
9
- } from "../../chunk-XHAETB7N.js";
9
+ } from "../../chunk-ZPQOZLB7.js";
10
10
  import {
11
11
  __toESM,
12
12
  init_esm_shims,
13
13
  readRepoText
14
- } from "../../chunk-ZNVILRHK.js";
14
+ } from "../../chunk-V3B2V4WA.js";
15
15
 
16
16
  // src/packs/decision-contracts/index.ts
17
17
  init_esm_shims();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cejel/cejel",
3
- "version": "0.4.7",
3
+ "version": "0.4.8",
4
4
  "mcpName": "io.github.BargLabs/cejel",
5
5
  "private": false,
6
6
  "publishConfig": {