@cejel/cejel 0.4.7 → 0.4.9

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,24 +5,26 @@ import {
5
5
  describeGitFailure,
6
6
  execGit,
7
7
  isExpectedGitAbsence
8
- } from "./chunk-OIGCYULH.js";
8
+ } from "./chunk-IHT7B6L7.js";
9
9
  import {
10
10
  external_exports
11
- } from "./chunk-UYJBSQCV.js";
11
+ } from "./chunk-GKYTLWQK.js";
12
12
  import {
13
13
  MAX_REPOSITORY_CONTENT_BYTES,
14
+ abstainSignalOnWithheldPaths,
15
+ classifyAbstentionReason,
14
16
  contentReadSignalKey,
15
17
  init_esm_shims,
16
18
  readRepoText,
17
19
  readRepoTextPrefix,
18
20
  recordContentSkip,
19
- recordContentSkipForCriteria,
20
21
  recordFilesystemSkip,
22
+ registerWithheldRepoPath,
21
23
  trackContentReads,
22
24
  withContentReadCriterion,
23
25
  withContentReadSignal,
24
26
  withContentReadSignals
25
- } from "./chunk-ZNVILRHK.js";
27
+ } from "./chunk-PTZU6G4E.js";
26
28
 
27
29
  // src/witan/schemas.ts
28
30
  init_esm_shims();
@@ -162,6 +164,13 @@ var WitanCriterionMetricSchema = external_exports.object({
162
164
  condition: external_exports.string().min(1).max(240).optional()
163
165
  }).strict().optional()
164
166
  }).strict();
167
+ var WitanContentReadSkipReasonSchema = external_exports.enum([
168
+ "unreadable",
169
+ "too_large",
170
+ "excluded_by_extension",
171
+ "denied_path",
172
+ "non_regular_file"
173
+ ]);
165
174
  var WitanCriterionSignalSchema = external_exports.object({
166
175
  criterionId: WitanCriterionIdSchema,
167
176
  positiveEvidence: external_exports.array(WitanEvidencePointerSchema).default([]),
@@ -171,9 +180,19 @@ var WitanCriterionSignalSchema = external_exports.object({
171
180
  notApplicable: external_exports.literal(true).optional(),
172
181
  // A collector reached relevant files but could not read their contents. This is a
173
182
  // measurement gap, never negative evidence: scoreCriterion maps it to insufficient_data.
174
- insufficientData: external_exports.literal(true).optional()
183
+ insufficientData: external_exports.literal(true).optional(),
184
+ // Why insufficientData is true. Omitted (or 'unreadable'/'denied_path') means an
185
+ // environmental read failure — evidence Cejel expected to see and could not; that stays in
186
+ // the composite denominator at score 0 so losing evidence never flatters the certificate
187
+ // (ADR-0001 does not cover this case — it is genuine evidence loss). 'too_large',
188
+ // 'excluded_by_extension', and 'non_regular_file' are self-imposed coverage limits Cejel
189
+ // chose itself: those are excluded from the composite exactly like ordinary
190
+ // insufficient_data (ADR-0001: coverage is disclosed, never discounts a score).
191
+ insufficientDataReason: WitanContentReadSkipReasonSchema.optional()
175
192
  }).strict().refine((signal) => !(signal.notApplicable && signal.insufficientData), {
176
193
  message: "A criterion cannot be both not applicable and insufficient data."
194
+ }).refine((signal) => signal.insufficientDataReason === void 0 || signal.insufficientData === true, {
195
+ message: "insufficientDataReason may only be set alongside insufficientData: true."
177
196
  });
178
197
  var WitanContentReadSummarySchema = external_exports.object({
179
198
  skipped: external_exports.number().int().nonnegative(),
@@ -296,12 +315,29 @@ var WitanReportCommonSchema = external_exports.object({
296
315
  productDisplayName: external_exports.string().min(1).max(120),
297
316
  repo: WitanRepoRefSchema,
298
317
  rubricVersion: external_exports.string().min(1).max(120),
318
+ // The BEHAVIOURAL identity of the rubric named above, beside its NAME. `rubricVersion` is an
319
+ // identifier the tool's author controls and can leave unchanged across a scoring change (this
320
+ // happened under witan-rubric-v17-2026-07-24 in 0.4.9 — see leaderboard/RUBRIC_CHANGELOG.md).
321
+ // This field is the sha256 of the scoring-relevant output of a fixed committed fixture corpus
322
+ // under that rubric, so two certificates naming the same rubric can be checked for behavioural
323
+ // agreement rather than assumed into it. See src/witan/rubric-fingerprint.ts for exactly what
324
+ // the digest covers, what it excludes, and what it does not claim. Optional and absent —
325
+ // never fabricated — for any rubric this build has no measurement for, and absent on every
326
+ // report emitted before report format 1.2; those artifacts' attestations remain valid.
327
+ rubricBehaviourFingerprint: external_exports.string().regex(/^sha256:[a-f0-9]{64}$/).optional(),
299
328
  // Rubric-driven: length matches whatever rubric produced this report, not a fixed enum.
300
329
  criteria: external_exports.array(WitanCriterionScoreSchema).min(1),
301
330
  consumedSignals: external_exports.array(WitanConsumedSignalSummarySchema).optional(),
302
331
  archetype: WitanRepoArchetypeSchema.optional(),
303
332
  scanLimitations: external_exports.array(external_exports.string().min(1).max(1e3)).max(16).optional(),
304
- contentReadSummary: WitanContentReadSummarySchema.optional()
333
+ contentReadSummary: WitanContentReadSummarySchema.optional(),
334
+ // The @cejel/cejel version that produced this report. Absent on reports from before this
335
+ // field existed and on any caller that does not supply one (createWitanReport never defaults
336
+ // it). Constant within an installed version, so re-running the same version on the same
337
+ // commit stays byte-stable (the exact property v0.3.1 removed generatedAt to protect); it
338
+ // differs across versions, which lets a consumer key incremental reuse on it
339
+ // (goal_cejel_0_4_8_abstention_scoring_fix_2026-09-08).
340
+ toolVersion: external_exports.string().min(1).max(80).optional()
305
341
  });
306
342
  var WitanScoredReportSchema = WitanReportCommonSchema.extend({
307
343
  verdict: WitanScoredVerdictSchema,
@@ -342,7 +378,7 @@ var WitanReportSchema = external_exports.preprocess((value) => {
342
378
  }, StrictWitanReportSchema);
343
379
  var WITAN_ATTESTATION_STATEMENT_TYPE = "https://in-toto.io/Statement/v1";
344
380
  var WITAN_ATTESTATION_PREDICATE_TYPE = "https://cejel.dev/attestations/scan/v1";
345
- var WITAN_REPORT_FORMAT_VERSION = "1.0";
381
+ var WITAN_REPORT_FORMAT_VERSION = "1.2";
346
382
  var WitanAttestationOutcomeSchema = external_exports.discriminatedUnion("status", [
347
383
  external_exports.object({
348
384
  status: external_exports.literal("scored"),
@@ -560,6 +596,7 @@ init_esm_shims();
560
596
  // src/witan/certificate-presentation.ts
561
597
  init_esm_shims();
562
598
  var CALLER_CONTEXT_PRODUCT_IDENTITY_NOTICE = "Product name and slug are caller context and are excluded from certificate byte-comparison claims.";
599
+ var CERTIFICATE_SCOPE_NOTICE = "This certificate is a statement about the repository tree at the revision pinned above, not about the system that tree is part of. Evidence outside that tree \u2014 for example, a test suite kept in a separate repository, or CI configuration that changes into a different working directory before running its checks \u2014 is neither seen by this scan nor claimed to be absent.";
563
600
  var CERTIFICATE_METRIC_REGISTRY = {
564
601
  test_to_source_ratio: { displayLabel: "Test-to-source file ratio" },
565
602
  coverage_percent: { displayLabel: "Static coverage percentage" },
@@ -1073,6 +1110,7 @@ function buildRelyingPartySummary(report) {
1073
1110
  "Anything absent from the measured evidence was not established: no measurement and no finding are not clearance."
1074
1111
  );
1075
1112
  return {
1113
+ scope: CERTIFICATE_SCOPE_NOTICE,
1076
1114
  examined: `Cejel examined the repository evidence recorded for ${report.productDisplayName}${revision} under ${report.rubricVersion}.`,
1077
1115
  established: `The report established measured results for ${measured.length} of ${applicable.length} applicable rubric dimensions and recorded ${findings} evidence-backed finding${findings === 1 ? "" : "s"}.`,
1078
1116
  notEstablished: gaps.join(" "),
@@ -1285,6 +1323,7 @@ function renderRelyingPartySummary(summary) {
1285
1323
  return `<section class="relying-party-summary" aria-labelledby="relying-party-heading">
1286
1324
  <h2 id="relying-party-heading">How to read this certificate</h2>
1287
1325
  <dl>
1326
+ <div><dt>Scope</dt><dd>${escapeHtml(summary.scope)}</dd></div>
1288
1327
  <div><dt>What was examined</dt><dd>${escapeHtml(summary.examined)}</dd></div>
1289
1328
  <div><dt>What was established</dt><dd>${escapeHtml(summary.established)}</dd></div>
1290
1329
  <div><dt>What was not established</dt><dd>${escapeHtml(summary.notEstablished)}</dd></div>
@@ -2007,29 +2046,56 @@ function inventoryAbsenceFinding(options) {
2007
2046
  } : {}
2008
2047
  };
2009
2048
  }
2010
- function wholesaleCriterionAbstention(signal) {
2049
+ function wholesaleCriterionAbstention(signal, reasons) {
2050
+ const { insufficientDataReason } = classifyAbstentionReason(reasons);
2011
2051
  return {
2012
2052
  criterionId: signal.criterionId,
2013
2053
  positiveEvidence: [],
2014
2054
  findings: [],
2015
2055
  metrics: [],
2016
2056
  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."
2057
+ ...insufficientDataReason !== void 0 ? { insufficientDataReason } : {},
2058
+ 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
2059
  };
2019
2060
  }
2061
+ function skipReasonsForCriterion(criterionId, tracked) {
2062
+ const combined = new Set(tracked.criterionSkipReasons.get(criterionId) ?? []);
2063
+ const prefix = contentReadSignalKey(criterionId, "");
2064
+ for (const [key, reasons] of tracked.signalSkipReasons) {
2065
+ if (!key.startsWith(prefix)) continue;
2066
+ for (const reason of reasons) combined.add(reason);
2067
+ }
2068
+ return combined;
2069
+ }
2070
+ function skipReasonsForSignal(criterionId, signalName, tracked) {
2071
+ const reasons = tracked.signalSkipReasons.get(contentReadSignalKey(criterionId, signalName));
2072
+ return reasons ? new Set(reasons) : void 0;
2073
+ }
2020
2074
  function buildWitanInputFromRepo(options) {
2021
2075
  const tracked = trackContentReads(options.repoPath, () => buildWitanInputFromRepoUntracked(options));
2022
2076
  const affectedCriteria = tracked.affectedCriteria;
2023
2077
  const rubricVersion = options.rubricVersion ?? WITAN_RUBRIC_VERSION;
2024
2078
  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;
2079
+ const readFailureCriteria = [];
2080
+ const coverageLimitedCriteria = [];
2081
+ const signals2 = (tracked.value.signals ?? []).map((signal) => {
2082
+ if (!affectedCriteria.has(signal.criterionId)) return signal;
2083
+ const reasons = skipReasonsForCriterion(signal.criterionId, tracked);
2084
+ const { insufficientDataReason } = classifyAbstentionReason(reasons);
2085
+ (insufficientDataReason !== void 0 ? coverageLimitedCriteria : readFailureCriteria).push(
2086
+ signal.criterionId
2087
+ );
2088
+ return wholesaleCriterionAbstention(signal, reasons);
2089
+ });
2029
2090
  const scanLimitations2 = [...tracked.value.scanLimitations ?? []];
2030
- if (affectedCount > 0 && scanLimitations2.length < 16) {
2091
+ if (readFailureCriteria.length > 0 && scanLimitations2.length < 16) {
2031
2092
  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.`
2093
+ `${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.`
2094
+ );
2095
+ }
2096
+ if (coverageLimitedCriteria.length > 0 && scanLimitations2.length < 16) {
2097
+ scanLimitations2.push(
2098
+ `${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
2099
  );
2034
2100
  }
2035
2101
  return {
@@ -2039,45 +2105,92 @@ function buildWitanInputFromRepo(options) {
2039
2105
  contentReadSummary: tracked.summary
2040
2106
  };
2041
2107
  }
2042
- const wholesaleAbstainedCriteria = [];
2043
- const partiallyAbstainedSignals = [];
2108
+ const wholesaleAbstainedReadFailureCriteria = [];
2109
+ const wholesaleAbstainedCoverageLimitedCriteria = [];
2110
+ const partiallyAbstainedReadFailureSignals = [];
2111
+ const partiallyAbstainedLargeFileSignals = [];
2112
+ const partiallyAbstainedOtherCoverageLimitedSignals = [];
2044
2113
  const signals = (tracked.value.signals ?? []).map((signal) => {
2045
2114
  if (!affectedCriteria.has(signal.criterionId)) return signal;
2046
2115
  if (tracked.unattributedCriteria.has(signal.criterionId)) {
2047
- wholesaleAbstainedCriteria.push(signal.criterionId);
2048
- return wholesaleCriterionAbstention(signal);
2116
+ const reasons = tracked.criterionSkipReasons.get(signal.criterionId);
2117
+ const { insufficientDataReason } = classifyAbstentionReason(reasons);
2118
+ (insufficientDataReason !== void 0 ? wholesaleAbstainedCoverageLimitedCriteria : wholesaleAbstainedReadFailureCriteria).push(signal.criterionId);
2119
+ return wholesaleCriterionAbstention(signal, reasons);
2049
2120
  }
2050
2121
  const prefix = contentReadSignalKey(signal.criterionId, "");
2051
2122
  const affectedNames = [...tracked.affectedSignals].filter((key) => key.startsWith(prefix)).map((key) => key.slice(prefix.length));
2052
2123
  if (affectedNames.length === 0) {
2053
- wholesaleAbstainedCriteria.push(signal.criterionId);
2124
+ wholesaleAbstainedReadFailureCriteria.push(signal.criterionId);
2054
2125
  return wholesaleCriterionAbstention(signal);
2055
2126
  }
2056
2127
  const affectedNameSet = new Set(affectedNames);
2057
2128
  const metrics = (signal.metrics ?? []).filter((metric2) => !affectedNameSet.has(metric2.name));
2058
2129
  const anythingSurvives = metrics.length > 0 || (signal.positiveEvidence?.length ?? 0) > 0 || (signal.findings?.length ?? 0) > 0;
2130
+ const droppedReasons = /* @__PURE__ */ new Set();
2131
+ for (const name of affectedNames) {
2132
+ for (const reason of skipReasonsForSignal(signal.criterionId, name, tracked) ?? []) {
2133
+ droppedReasons.add(reason);
2134
+ }
2135
+ }
2059
2136
  if (!anythingSurvives) {
2060
- wholesaleAbstainedCriteria.push(signal.criterionId);
2061
- return wholesaleCriterionAbstention(signal);
2137
+ const { insufficientDataReason } = classifyAbstentionReason(droppedReasons);
2138
+ (insufficientDataReason !== void 0 ? wholesaleAbstainedCoverageLimitedCriteria : wholesaleAbstainedReadFailureCriteria).push(signal.criterionId);
2139
+ return wholesaleCriterionAbstention(signal, droppedReasons);
2062
2140
  }
2141
+ const nameBuckets = /* @__PURE__ */ new Map();
2063
2142
  for (const name of affectedNames) {
2064
- partiallyAbstainedSignals.push(`${signal.criterionId}.${name}`);
2065
- }
2143
+ const nameReasons = skipReasonsForSignal(signal.criterionId, name, tracked);
2144
+ const { insufficientDataReason: nameReason } = classifyAbstentionReason(nameReasons);
2145
+ const bucket = nameReason === void 0 ? "read-failure" : nameReason === "too_large" ? "too-large" : "other-coverage";
2146
+ nameBuckets.set(name, bucket);
2147
+ (bucket === "read-failure" ? partiallyAbstainedReadFailureSignals : bucket === "too-large" ? partiallyAbstainedLargeFileSignals : partiallyAbstainedOtherCoverageLimitedSignals).push(`${signal.criterionId}.${name}`);
2148
+ }
2149
+ const distinctBuckets = new Set(nameBuckets.values());
2150
+ const uniformBucket = distinctBuckets.size === 1 ? [...distinctBuckets][0] : void 0;
2151
+ const affectedList = [...affectedNames].sort().join(", ");
2152
+ const notes = uniformBucket === "read-failure" ? `Cejel abstained on ${affectedList} in this criterion because that content could not be read; other signals in this criterion were computed from readable files and are unaffected.` : uniformBucket === "too-large" ? `Cejel declined ${affectedList} in this criterion under the repository content size limit; other signals in this criterion were computed from readable files and are unaffected. This is a disclosed coverage limit, not a read failure.` : uniformBucket === "other-coverage" ? `Cejel declined ${affectedList} in this criterion under an extension exclusion or a non-regular-file skip; other signals in this criterion were computed from readable files and are unaffected. This is a disclosed coverage limit, not a read failure.` : `Cejel abstained on ${affectedList} in this criterion for a mix of reasons \u2014 some content could not be read, some was declined under the repository content size limit or an extension exclusion; other signals in this criterion were computed from readable files and are unaffected.`;
2066
2153
  return {
2067
2154
  ...signal,
2068
2155
  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.`
2156
+ notes
2070
2157
  };
2071
2158
  });
2072
2159
  const scanLimitations = [...tracked.value.scanLimitations ?? []];
2073
- if (wholesaleAbstainedCriteria.length > 0 && scanLimitations.length < 16) {
2074
- 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.`
2160
+ const candidateScanLimitations = [];
2161
+ if (wholesaleAbstainedReadFailureCriteria.length > 0) {
2162
+ candidateScanLimitations.push(
2163
+ `${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.`
2164
+ );
2165
+ }
2166
+ if (wholesaleAbstainedCoverageLimitedCriteria.length > 0) {
2167
+ candidateScanLimitations.push(
2168
+ `${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.`
2169
+ );
2170
+ }
2171
+ if (partiallyAbstainedReadFailureSignals.length > 0) {
2172
+ candidateScanLimitations.push(
2173
+ `${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
2174
  );
2077
2175
  }
2078
- if (partiallyAbstainedSignals.length > 0 && scanLimitations.length < 16) {
2176
+ if (partiallyAbstainedLargeFileSignals.length > 0) {
2177
+ candidateScanLimitations.push(
2178
+ `${partiallyAbstainedLargeFileSignals.length} ${partiallyAbstainedLargeFileSignals.length === 1 ? "signal" : "signals"} declined a large implementation file that exceeded the repository content size limit, while other signals in the same criterion were unaffected: ${[...partiallyAbstainedLargeFileSignals].sort().join(", ")}. That file is undercounted, not scored as clean or absent \u2014 this is a disclosed coverage limit, not a read failure.`
2179
+ );
2180
+ }
2181
+ if (partiallyAbstainedOtherCoverageLimitedSignals.length > 0) {
2182
+ candidateScanLimitations.push(
2183
+ `${partiallyAbstainedOtherCoverageLimitedSignals.length} ${partiallyAbstainedOtherCoverageLimitedSignals.length === 1 ? "signal" : "signals"} declined under an extension exclusion or a non-regular-file skip, while other signals in the same criterion were unaffected: ${[...partiallyAbstainedOtherCoverageLimitedSignals].sort().join(", ")}. This is a disclosed coverage limit, not a read failure.`
2184
+ );
2185
+ }
2186
+ const availableSlots = Math.max(0, 16 - scanLimitations.length);
2187
+ if (candidateScanLimitations.length <= availableSlots) {
2188
+ scanLimitations.push(...candidateScanLimitations);
2189
+ } else if (availableSlots > 0) {
2190
+ const keep = availableSlots - 1;
2191
+ scanLimitations.push(...candidateScanLimitations.slice(0, keep));
2079
2192
  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(", ")}.`
2193
+ `${candidateScanLimitations.length - keep} additional disclosed scan limitation(s) omitted at the 16-entry cap \u2014 see contentReadSummary for full counts.`
2081
2194
  );
2082
2195
  }
2083
2196
  return {
@@ -2107,6 +2220,8 @@ function buildWitanInputFromRepoUntracked(options) {
2107
2220
  const usesV21ExecutedEscalations = rubricVersion === WITAN_RUBRIC_VERSION_V21 || rubricVersion === WITAN_RUBRIC_VERSION_V22 || rubricVersion === WITAN_RUBRIC_VERSION_V23;
2108
2221
  const usesV22PackageStartEntrypoint = rubricVersion === WITAN_RUBRIC_VERSION_V22 || rubricVersion === WITAN_RUBRIC_VERSION_V23;
2109
2222
  const usesV23CommandCoverage = rubricVersion === WITAN_RUBRIC_VERSION_V23;
2223
+ const usesV23PemPrivateKeyGrammar = rubricVersion === WITAN_RUBRIC_VERSION_V23;
2224
+ const usesV23WithheldPathAbstention = rubricVersion === WITAN_RUBRIC_VERSION_V23;
2110
2225
  const structuralArchetype = classifyRepoArchetype(inventoryFiles, rubricVersion);
2111
2226
  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
2227
  options.repoPath,
@@ -2143,6 +2258,8 @@ function buildWitanInputFromRepoUntracked(options) {
2143
2258
  usesV21ExecutedEscalations,
2144
2259
  usesV22PackageStartEntrypoint,
2145
2260
  usesV23CommandCoverage,
2261
+ usesV23PemPrivateKeyGrammar,
2262
+ usesV23WithheldPathAbstention,
2146
2263
  reviewableSourceProof,
2147
2264
  inventoryAbsenceContext,
2148
2265
  scanLimitations
@@ -2754,7 +2871,7 @@ function buildNotApplicableSignal(criterionId, reason) {
2754
2871
  notApplicable: true
2755
2872
  };
2756
2873
  }
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()) {
2874
+ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV18NativeRls, useV19CommitYear, useV20A3ExplicitGaps, useV21ExecutedEscalations, useV22PackageStartEntrypoint, useV23CommandCoverage, useV23PemPrivateKeyGrammar, useV23WithheldPathAbstention, reviewableSourceProof, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT, scanLimitations = /* @__PURE__ */ new Set()) {
2758
2875
  const signals = [];
2759
2876
  const mono = resolveMonorepoContext(repoPath, scanLimitations);
2760
2877
  const collectCriterion = (criterionId, collect) => withContentReadCriterion(criterionId, collect);
@@ -2781,6 +2898,7 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
2781
2898
  useV36Detectors,
2782
2899
  useV39Detectors,
2783
2900
  useV47Detectors,
2901
+ useV23PemPrivateKeyGrammar,
2784
2902
  useV18NativeRls,
2785
2903
  inventoryAbsenceContext,
2786
2904
  scanLimitations
@@ -2794,6 +2912,7 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
2794
2912
  useV27Detectors,
2795
2913
  useV20A3ExplicitGaps,
2796
2914
  useV22PackageStartEntrypoint,
2915
+ useV23WithheldPathAbstention,
2797
2916
  inventoryAbsenceContext
2798
2917
  )
2799
2918
  );
@@ -2814,6 +2933,7 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
2814
2933
  repoPath,
2815
2934
  repoFiles,
2816
2935
  useV27Detectors,
2936
+ useV23WithheldPathAbstention,
2817
2937
  inventoryAbsenceContext
2818
2938
  )
2819
2939
  );
@@ -2916,12 +3036,16 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
2916
3036
  const ciHasTypecheckCommand = ciWorkflows.some(
2917
3037
  (file) => fileContains(repoPath, file, CI_TYPECHECK_COMMAND_PATTERN)
2918
3038
  );
2919
- const verificationScriptCount = [
2920
- packageScripts.has("test") || Boolean(ciTestWorkflow),
2921
- coverageAnalysis.hasCoverageCommand,
2922
- packageScripts.has("lint") || ciHasLintCommand,
2923
- packageScripts.has("typecheck") || ciHasTypecheckCommand
2924
- ].filter(Boolean).length;
3039
+ const verificationScriptComponents = [
3040
+ { label: "test command", count: hasCredibleTestScript(packageScripts) || Boolean(ciTestWorkflow) ? 1 : 0 },
3041
+ { label: "coverage command", count: coverageAnalysis.hasCoverageCommand ? 1 : 0 },
3042
+ { label: "lint command", count: packageScripts.has("lint") || ciHasLintCommand ? 1 : 0 },
3043
+ {
3044
+ label: "type-check command",
3045
+ count: packageScripts.has("typecheck") || ciHasTypecheckCommand ? 1 : 0
3046
+ }
3047
+ ];
3048
+ const verificationScriptCount = verificationScriptComponents.filter((c) => c.count > 0).length;
2925
3049
  const usesLeanBuiltInTestRunner = packageJson != null && [...packageScripts.values()].some(
2926
3050
  (script) => LEAN_TEST_RUNNER_SCRIPT_PATTERN.test(script)
2927
3051
  ) || testFiles.some((file) => fileContains(repoPath, file, LEAN_TEST_RUNNER_IMPORT_PATTERN));
@@ -2985,7 +3109,9 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
2985
3109
  repoPath,
2986
3110
  reviewableSourceProof,
2987
3111
  repoFiles.length,
2988
- inventoryAbsenceContext
3112
+ inventoryAbsenceContext,
3113
+ verificationScriptComponents,
3114
+ runnerFiles.length
2989
3115
  ) : null;
2990
3116
  }
2991
3117
  if (testFiles.length === 0) {
@@ -3085,19 +3211,7 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
3085
3211
  "saturating_count",
3086
3212
  {
3087
3213
  components: [
3088
- {
3089
- label: "test command",
3090
- count: packageScripts.has("test") || Boolean(ciTestWorkflow) ? 1 : 0
3091
- },
3092
- { label: "coverage command", count: coverageAnalysis.hasCoverageCommand ? 1 : 0 },
3093
- {
3094
- label: "lint command",
3095
- count: packageScripts.has("lint") || ciHasLintCommand ? 1 : 0
3096
- },
3097
- {
3098
- label: "type-check command",
3099
- count: packageScripts.has("typecheck") || ciHasTypecheckCommand ? 1 : 0
3100
- },
3214
+ ...verificationScriptComponents,
3101
3215
  { label: "test-runner configuration", count: runnerFiles.length }
3102
3216
  ]
3103
3217
  }
@@ -3115,7 +3229,7 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
3115
3229
  notes: coverageFiles.length === 0 && coverageAnalysis.unresolvedFiles.length > 0 ? `A1 is detected from real test files, test runner configuration, and optional coverage configuration. Coverage command reachability or flag ownership was unresolved in ${coverageAnalysis.unresolvedFiles.slice(0, 4).join(", ")}; the no-coverage absence claim was withheld.` : "A1 is detected from real test files, test runner configuration, and optional coverage configuration."
3116
3230
  };
3117
3231
  }
3118
- function buildA1AuthenticatedAbsenceSignal(repoPath, proof, inventoryCount, inventoryAbsenceContext) {
3232
+ function buildA1AuthenticatedAbsenceSignal(repoPath, proof, inventoryCount, inventoryAbsenceContext, verificationScriptComponents, runnerFileCount) {
3119
3233
  const sourcePath = proof.firstReadablePath;
3120
3234
  if (!sourcePath) {
3121
3235
  throw new Error("Cejel invariant: a passing reviewable-source proof must have a readable path");
@@ -3167,7 +3281,12 @@ function buildA1AuthenticatedAbsenceSignal(repoPath, proof, inventoryCount, inve
3167
3281
  metric(
3168
3282
  "verification_script_ratio",
3169
3283
  "Verification script ratio",
3170
- 0,
3284
+ // Zero test FILES does not mean zero verification signal: a lint/typecheck/coverage
3285
+ // script or CI command is a real, independently-observable fact from package.json/CI
3286
+ // content and must be credited here the same way B3 credits it, or A1 flatly
3287
+ // contradicts B3 on the same repository fact (goal_cejel_detector_false_assertions_
3288
+ // 0_4_9_2026-09-14 finding 3).
3289
+ verificationScriptComponents.filter((c) => c.count > 0).length + runnerFileCount,
3171
3290
  4,
3172
3291
  0.25,
3173
3292
  "ratio",
@@ -3175,11 +3294,8 @@ function buildA1AuthenticatedAbsenceSignal(repoPath, proof, inventoryCount, inve
3175
3294
  "saturating_count",
3176
3295
  {
3177
3296
  components: [
3178
- { label: "test command", count: 0 },
3179
- { label: "coverage command", count: 0 },
3180
- { label: "lint command", count: 0 },
3181
- { label: "type-check command", count: 0 },
3182
- { label: "test-runner configuration", count: 0 }
3297
+ ...verificationScriptComponents,
3298
+ { label: "test-runner configuration", count: runnerFileCount }
3183
3299
  ]
3184
3300
  }
3185
3301
  ),
@@ -3193,10 +3309,10 @@ function buildA1AuthenticatedAbsenceSignal(repoPath, proof, inventoryCount, inve
3193
3309
  "No concrete test files were present to establish non-hollow test behavior."
3194
3310
  )
3195
3311
  ],
3196
- notes: `V17 authenticated absence: ${proof.eligibleSourceFileCount} criterion-ratable of ${proof.sourceShapedFileCount} source-shaped files; sampled ${proof.sampledFileCount}; ${proof.readableSampledFileCount} representative-text files.`
3312
+ notes: `V17 authenticated absence: ${proof.eligibleSourceFileCount} criterion-ratable of ${proof.sourceShapedFileCount} source-shaped files; sampled ${proof.sampledFileCount}; ${proof.readableSampledFileCount} representative-text files. Verification-script signals (lint/typecheck/coverage/CI commands) are independent of test-file presence and are still credited above where detected.`
3197
3313
  };
3198
3314
  }
3199
- function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV18NativeRls, inventoryAbsenceContext, scanLimitations) {
3315
+ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV23PemPrivateKeyGrammar, useV18NativeRls, inventoryAbsenceContext, scanLimitations) {
3200
3316
  const evidence = [];
3201
3317
  const findings = [];
3202
3318
  const gitignore = repoFiles.find((file) => basename(file) === ".gitignore");
@@ -3219,7 +3335,8 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
3219
3335
  path,
3220
3336
  useV36Detectors,
3221
3337
  useV39Detectors,
3222
- useV47Detectors
3338
+ useV47Detectors,
3339
+ useV23PemPrivateKeyGrammar
3223
3340
  );
3224
3341
  if (!found) continue;
3225
3342
  match_ = { path, match: found };
@@ -3227,7 +3344,9 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
3227
3344
  }
3228
3345
  const fingerprintsByPath = /* @__PURE__ */ new Map();
3229
3346
  if (useV27Detectors) {
3230
- for (const path of repoFiles.filter((candidate) => isCredentialHistoryPath(candidate))) {
3347
+ for (const path of repoFiles.filter(
3348
+ (candidate) => isCredentialHistoryPath(candidate, useV36Detectors, useV23PemPrivateKeyGrammar)
3349
+ )) {
3231
3350
  fingerprintsByPath.set(
3232
3351
  path,
3233
3352
  new Set(
@@ -3236,7 +3355,8 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
3236
3355
  path,
3237
3356
  useV36Detectors,
3238
3357
  useV39Detectors,
3239
- useV47Detectors
3358
+ useV47Detectors,
3359
+ useV23PemPrivateKeyGrammar
3240
3360
  )
3241
3361
  )
3242
3362
  );
@@ -3253,7 +3373,8 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
3253
3373
  useV33Detectors,
3254
3374
  useV36Detectors,
3255
3375
  useV39Detectors,
3256
- useV47Detectors
3376
+ useV47Detectors,
3377
+ useV23PemPrivateKeyGrammar
3257
3378
  );
3258
3379
  const hasConfirmedSecretFinding = committedSecret != null || historySecretScan?.evidence != null;
3259
3380
  const cryptoHygiene = withContentReadSignal(
@@ -3380,15 +3501,16 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
3380
3501
  if (committedSecret) {
3381
3502
  const isTestPath = isTestOrFixturePath(committedSecret.path);
3382
3503
  const isDefaultAdmin = committedSecret.match.kind === "default_admin";
3504
+ const isPemPrivateKey = committedSecret.match.kind === "pem_private_key";
3383
3505
  findings.push({
3384
3506
  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.",
3507
+ 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
3508
  evidence: evidenceForRelativeAtLine(
3387
3509
  repoPath,
3388
3510
  committedSecret.path,
3389
3511
  "secret_scan",
3390
3512
  secretEvidenceLabel(
3391
- isDefaultAdmin ? "Committed default administrative credential" : "Committed secret-shaped value",
3513
+ isDefaultAdmin ? "Committed default administrative credential" : isPemPrivateKey ? "Committed PEM-formatted private key" : "Committed secret-shaped value",
3392
3514
  committedSecret.match
3393
3515
  ),
3394
3516
  committedSecret.match.line
@@ -3419,9 +3541,10 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
3419
3541
  if (historySecretScan?.evidence) {
3420
3542
  const historyPath = historySecretScan.evidence.path ?? "";
3421
3543
  const isTestPath = isTestOrFixturePath(historyPath);
3544
+ const isPemPrivateKey = historySecretScan.secretKind === "pem_private_key";
3422
3545
  findings.push({
3423
3546
  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.",
3547
+ 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
3548
  evidence: historySecretScan.evidence
3426
3549
  });
3427
3550
  } else if (historySecretScan?.truncated) {
@@ -3646,7 +3769,7 @@ function collectCryptoHygieneEvidence(repoPath, implFiles, useV39Detectors = fal
3646
3769
  }
3647
3770
  return { evidence, findings, hasSurface, clean: hasSurface && !insecureFound };
3648
3771
  }
3649
- function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, useV20ExplicitGaps = false, useV22PackageStartEntrypoint = false, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT) {
3772
+ function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, useV20ExplicitGaps = false, useV22PackageStartEntrypoint = false, useV23WithheldPathAbstention = false, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT) {
3650
3773
  const v20DirectHttpEntrypoint = useV20ExplicitGaps ? findV20DirectHttpServerEntrypointFile(repoPath, repoFiles) : null;
3651
3774
  const v22PackageStartHttpEntrypoint = useV22PackageStartEntrypoint ? findV22PackageStartHttpServerEntrypointFile(repoPath, repoFiles) : null;
3652
3775
  const v20RuntimeContainer = useV20ExplicitGaps ? findRuntimeContainerEntrypointFile(repoPath, repoFiles, true) : null;
@@ -3684,9 +3807,11 @@ function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, us
3684
3807
  )
3685
3808
  );
3686
3809
  const healthCheck = healthChecks[0];
3810
+ const healthReadinessRouteReads = (file) => isAuthoredProductionPath(file) && isImplementationFile(file);
3811
+ const observabilityDepthReads = (file) => (!useV27Detectors || isAuthoredProductionPath(file)) && isImplementationFile(file);
3687
3812
  const { hasV20HealthOrReadinessRoute, hasV22PackageStartHealthOrReadinessRoute } = withContentReadSignal("A3", "health_readiness_route", () => ({
3688
3813
  hasV20HealthOrReadinessRoute: useV20ExplicitGaps && repoFiles.some(
3689
- (file) => isAuthoredProductionPath(file) && isImplementationFile(file) && fileContains(repoPath, file, V20_HEALTH_OR_READINESS_ROUTE_PATTERN)
3814
+ (file) => healthReadinessRouteReads(file) && fileContains(repoPath, file, V20_HEALTH_OR_READINESS_ROUTE_PATTERN)
3690
3815
  ),
3691
3816
  hasV22PackageStartHealthOrReadinessRoute: v22PackageStartHttpEntrypoint !== null && fileContains(
3692
3817
  repoPath,
@@ -3694,23 +3819,34 @@ function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, us
3694
3819
  V20_HEALTH_OR_READINESS_ROUTE_PATTERN
3695
3820
  )
3696
3821
  }));
3822
+ const healthReadinessRouteWithheld = useV23WithheldPathAbstention && useV20ExplicitGaps && abstainSignalOnWithheldPaths("A3", "health_readiness_route", healthReadinessRouteReads);
3697
3823
  const serverEntrypoint = v22PackageStartHttpEntrypoint ?? findServerEntrypointFile(repoPath, repoFiles, useV27Detectors) ?? v20DirectHttpEntrypoint ?? null;
3698
3824
  const runtimeContainer = useV27Detectors ? findRuntimeContainerEntrypointFile(repoPath, repoFiles, useV20ExplicitGaps) : null;
3699
3825
  const errorBoundaries = repoFiles.filter(
3700
3826
  (file) => (!useV27Detectors || isAuthoredProductionPath(file)) && /error-boundary|error\.(tsx|jsx|ts|js)$/.test(file)
3701
3827
  );
3702
- const errorBoundary = errorBoundaries[0];
3828
+ const expressErrorMiddlewareFile = errorBoundaries.length > 0 ? void 0 : withContentReadSignal(
3829
+ "A3",
3830
+ "prod_readiness_primitives",
3831
+ () => repoFiles.find(
3832
+ (file) => observabilityDepthReads(file) && fileContains(repoPath, file, EXPRESS_ERROR_MIDDLEWARE_PATTERN) && fileContains(repoPath, file, EXPRESS_MIDDLEWARE_REACHABLE_PATTERN)
3833
+ )
3834
+ );
3835
+ const errorBoundary = errorBoundaries[0] ?? expressErrorMiddlewareFile;
3836
+ if (useV23WithheldPathAbstention && errorBoundaries.length === 0) {
3837
+ abstainSignalOnWithheldPaths("A3", "prod_readiness_primitives", observabilityDepthReads);
3838
+ }
3703
3839
  const observabilityCount = withContentReadSignal(
3704
3840
  "A3",
3705
3841
  "observability_depth",
3706
- () => countFilesContaining(
3707
- repoPath,
3708
- repoFiles.filter(
3709
- (file) => (!useV27Detectors || isAuthoredProductionPath(file)) && isImplementationFile(file)
3710
- ),
3711
- /sentry|otel|opentelemetry|datadog|prometheus|metrics|logger|logtail/i
3712
- )
3842
+ () => countFilesMatchingAny(repoPath, repoFiles.filter(observabilityDepthReads), [
3843
+ /sentry|otel|opentelemetry|datadog|prometheus|metrics|logger|logtail|\b(?:correlation|request|trace)[-_]?id\b|\basynclocalstorage\b/i,
3844
+ /\b(?:pino|winston|bunyan|morgan)\b/
3845
+ ])
3713
3846
  );
3847
+ if (useV23WithheldPathAbstention) {
3848
+ abstainSignalOnWithheldPaths("A3", "observability_depth", observabilityDepthReads);
3849
+ }
3714
3850
  const rollbackSafetyCount = withContentReadSignal(
3715
3851
  "A3",
3716
3852
  "rollback_safety_depth",
@@ -3757,8 +3893,17 @@ function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, us
3757
3893
  )
3758
3894
  );
3759
3895
  }
3760
- if (errorBoundary)
3761
- evidence.push(evidenceForRelative(repoPath, errorBoundary, "prod_check", "Error boundary"));
3896
+ if (errorBoundary) {
3897
+ evidence.push(
3898
+ errorBoundary === expressErrorMiddlewareFile ? evidenceForRelativeAtLine(
3899
+ repoPath,
3900
+ errorBoundary,
3901
+ "prod_check",
3902
+ "Error boundary",
3903
+ findFirstMatchingLine(repoPath, errorBoundary, EXPRESS_ERROR_MIDDLEWARE_PATTERN)
3904
+ ) : evidenceForRelative(repoPath, errorBoundary, "prod_check", "Error boundary")
3905
+ );
3906
+ }
3762
3907
  if (evidence.length === 0) {
3763
3908
  if (serverEntrypoint) {
3764
3909
  evidence.push(
@@ -3812,7 +3957,12 @@ function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, us
3812
3957
  )
3813
3958
  });
3814
3959
  }
3815
- } else if (serverEntrypoint && withContentReadSignal(
3960
+ } else if (serverEntrypoint && // An absence finding is an assertion that Cejel looked and found nothing. When a file this
3961
+ // signal would have read was withheld, Cejel did not look at everything it selected, so the
3962
+ // honest output is the recorded abstention above and no finding — never "declares no health
3963
+ // or readiness route". This is the whole defect: insufficient_data must not be emitted as a
3964
+ // finding of absence.
3965
+ !healthReadinessRouteWithheld && withContentReadSignal(
3816
3966
  "A3",
3817
3967
  "health_readiness_route",
3818
3968
  () => fileContains(
@@ -3866,7 +4016,7 @@ function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, us
3866
4016
  deployConfigs.length > 0,
3867
4017
  envTemplate,
3868
4018
  healthChecks.length > 0,
3869
- errorBoundaries.length > 0
4019
+ errorBoundary !== void 0
3870
4020
  ].filter(Boolean).length,
3871
4021
  6,
3872
4022
  0.55,
@@ -3883,7 +4033,7 @@ function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, us
3883
4033
  { label: "deployment configuration", count: deployConfigs.length > 0 ? 1 : 0 },
3884
4034
  { label: "environment template", count: envTemplate ? 1 : 0 },
3885
4035
  { label: "health or readiness signal", count: healthChecks.length > 0 ? 1 : 0 },
3886
- { label: "error boundary", count: errorBoundaries.length > 0 ? 1 : 0 }
4036
+ { label: "error boundary", count: errorBoundary !== void 0 ? 1 : 0 }
3887
4037
  ]
3888
4038
  }
3889
4039
  ),
@@ -4127,7 +4277,26 @@ function hasDeclaredVersionConstraint(dependency) {
4127
4277
  if (/[~^<>=!]/.test(trimmed)) return true;
4128
4278
  return /^v?\d/.test(trimmed);
4129
4279
  }
4130
- function collectA5ClaimRealityEvidence(repoPath, repoFiles, useV27Detectors, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT) {
4280
+ function collectA5ClaimRealityEvidence(repoPath, repoFiles, useV27Detectors, useV23WithheldPathAbstention, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT) {
4281
+ const claimImplementationReads = claimImplementationFileReads(useV27Detectors);
4282
+ if (useV23WithheldPathAbstention) {
4283
+ abstainSignalOnWithheldPaths(
4284
+ "A5",
4285
+ "claim_match_rate",
4286
+ (file) => claimImplementationReads(file) || isClaimSourceFile(file)
4287
+ );
4288
+ abstainSignalOnWithheldPaths("A5", "claim_source_depth", isClaimSourceFile);
4289
+ abstainSignalOnWithheldPaths(
4290
+ "A5",
4291
+ "reconciliation_artifact_depth",
4292
+ isClaimRealityReconciliationPath
4293
+ );
4294
+ abstainSignalOnWithheldPaths(
4295
+ "A5",
4296
+ "negative_space_documentation",
4297
+ isNegativeSpaceDocCandidate
4298
+ );
4299
+ }
4131
4300
  const reconciliationArtifacts = useV27Detectors ? findClaimRealityReconciliationArtifacts(repoPath, repoFiles) : [];
4132
4301
  const claimSourceFiles = useV27Detectors ? findClaimSourceFiles(repoFiles) : repoFiles.filter(
4133
4302
  (file) => /^(?:README|readme)\.md$/.test(file) || /^docs\/[^/]+\.(?:md|mdx)$/.test(file)
@@ -4173,7 +4342,7 @@ function collectA5ClaimRealityEvidence(repoPath, repoFiles, useV27Detectors, inv
4173
4342
  const negativeSpaceCandidates = /* @__PURE__ */ new Set([
4174
4343
  claimDoc,
4175
4344
  ...allDocFiles,
4176
- ...repoFiles.filter((file) => /(^|\/)(SECURITY|THREAT[_-]?MODEL)\.md$/i.test(file))
4345
+ ...repoFiles.filter(isSecurityOrThreatModelDoc)
4177
4346
  ]);
4178
4347
  const negativeSpaceDoc = withContentReadSignal(
4179
4348
  "A5",
@@ -4261,7 +4430,7 @@ function collectA5ClaimRealityEvidence(repoPath, repoFiles, useV27Detectors, inv
4261
4430
  function collectB2PrTraceEvidence(repoPath, repoFiles) {
4262
4431
  const workflows = repoFiles.filter(isCiWorkflow);
4263
4432
  const prTemplate = repoFiles.find(
4264
- (file) => /(^|\/)(pull_request_template|PULL_REQUEST_TEMPLATE)\.md$/.test(file)
4433
+ (file) => /(^|\/)(pull_request_template|PULL_REQUEST_TEMPLATE)(\.md$|\/[^/]+\.md$)/.test(file)
4265
4434
  );
4266
4435
  const branchProtectionDoc = repoFiles.find(
4267
4436
  (file) => /branch.*protection|review.*gate|CODEOWNERS/i.test(file)
@@ -4364,7 +4533,9 @@ function collectB3CiDisciplineEvidence(repoPath, repoFiles, mono) {
4364
4533
  CI_TYPECHECK_COMMAND_PATTERN,
4365
4534
  CI_BUILD_COMMAND_PATTERN
4366
4535
  ];
4367
- const scriptDepth = ["test", "lint", "typecheck", "build"].filter((s) => scripts.has(s)).length;
4536
+ const scriptDepth = ["test", "lint", "typecheck", "build"].filter(
4537
+ (s) => s === "test" ? hasCredibleTestScript(scripts) : scripts.has(s)
4538
+ ).length;
4368
4539
  const { repoCategoryHits, rootCategoryHits } = withContentReadSignal(
4369
4540
  "B3",
4370
4541
  "ci_script_depth",
@@ -4397,7 +4568,7 @@ function collectB3CiDisciplineEvidence(repoPath, repoFiles, mono) {
4397
4568
  components: [
4398
4569
  ...["test", "lint", "typecheck", "build"].map((name) => ({
4399
4570
  label: `package script: ${name === "typecheck" ? "type-check" : name}`,
4400
- count: scripts.has(name) ? 1 : 0
4571
+ count: (name === "test" ? hasCredibleTestScript(scripts) : scripts.has(name)) ? 1 : 0
4401
4572
  })),
4402
4573
  ...CI_COMMAND_CATEGORIES.map((_pattern, index) => ({
4403
4574
  label: `repository CI command: ${["test", "lint", "type-check", "build"][index]}`,
@@ -4756,6 +4927,10 @@ function isTestFile(file, rubricVersion = WITAN_RUBRIC_VERSION) {
4756
4927
  var NAME_SHAPED_TEST_FILE_PATTERN = /^test(?:-[^/]+)?\.[cm]?[jt]sx?$|\.(test|spec)\.[cm]?[jt]sx?$|(^|\/)__tests__\/|(^|\/)test_[^/]*\.py$|(^|\/)tests?\.py$|_test\.go$|(^|\/)test_[^/]*\.(cpp|cc|cxx)$|_(test|tests)\.(cpp|cc|cxx)$|Tests?\.(java|kt)$|(_test|_spec)\.(rs|rb|php|swift|kt)$/;
4757
4928
  var NAME_SHAPED_TEST_FILE_PATTERN_V17 = /\.tftest\.hcl$|\.bats$|(^|\/)test[^/]*\.m$|_test\.m$/i;
4758
4929
  var TEST_RUNNER_PATTERN = /\b(vitest|jest|mocha|ava|tap|pytest|go test|cargo test|rspec|phpunit|gradle test|mvn test|node\s+--test|node:test)\b/i;
4930
+ function hasCredibleTestScript(scripts) {
4931
+ const script = scripts.get("test");
4932
+ return script != null && TEST_RUNNER_PATTERN.test(script);
4933
+ }
4759
4934
  var CI_TEST_COMMAND_PATTERN = /\b(pytest|go\s+test|cargo\s+test|mvn\s+test|gradle\s+test|dotnet\s+test|ctest|make\s+test|jest|vitest|mocha|rspec|phpunit|pnpm\s+(?:run\s+)?test|npm\s+(?:run\s+)?test|yarn\s+(?:run\s+)?test|node\s+--test|node:test)\b|--target[=\s]+test\b/i;
4760
4935
  var CI_LINT_COMMAND_PATTERN = /\b(eslint|pylint|flake8|black|ruff)\b/i;
4761
4936
  var CI_TYPECHECK_COMMAND_PATTERN = /\b(tsc|mypy|pyright)\b/i;
@@ -4803,6 +4978,7 @@ var GATED_PRIVILEGE_CHECK_PATTERN = /pg_has_role\s*\(|has_role\s*\(|is not a mem
4803
4978
  var SET_ROLE_PATTERN = /\bset\s+(local\s+)?role\b/i;
4804
4979
  var REQUIRED_REVIEW_PATTERN = /required review|branch protection|protected branch/i;
4805
4980
  var HISTORY_SECRET_SCAN_CREDENTIAL_BLOB_LIMIT = 5e3;
4981
+ var V23_PEM_WIDENED_HISTORY_BLOB_LIMIT = 500;
4806
4982
  var SECRET_IDENTIFIER_KEYWORD_PATTERN = /secret|token|api[_-]?key|password|access[_-]?key/gi;
4807
4983
  var SINGLE_WHITESPACE_PATTERN = /\s/u;
4808
4984
  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,})$/;
@@ -4928,8 +5104,8 @@ function looksLikeSecretValue(value) {
4928
5104
  return SECRET_VALUE_BRANDED_PATTERN.test(value) || isGenericSecretValue(value);
4929
5105
  }
4930
5106
  var PLACEHOLDER_SECRET_PATTERN = /(?:^|[-_])(?:your|example|sample|placeholder|dummy|changeme|replace(?:-?me)?|redacted)(?:$|[-_])|^x{3,}$|^\.\.\.|^\$\{[^}]+}$|^<[^>]+>$/i;
4931
- var ENV_TEMPLATE_PATTERN = /(^|\/)\.env(?:\.[^/]+)*\.(?:example|sample|template|dist)$/i;
4932
- var ENV_TEMPLATE_PATTERN_V47 = /(^|\/)\.env(?:\.[^/]+)*\.(?:example|sample|template|dist|exemplo|ejemplo|exemple|esempio|beispiel|voorbeeld)$/i;
5107
+ var ENV_TEMPLATE_PATTERN = /(^|\/)\.env(?:\.[^/]+)?\.(?:example|sample|template|dist)$/i;
5108
+ var ENV_TEMPLATE_PATTERN_V47 = /(^|\/)\.env(?:\.[^/]+)?\.(?:example|sample|template|dist|exemplo|ejemplo|exemple|esempio|beispiel|voorbeeld)$/i;
4933
5109
  function isEnvTemplatePath(path, useV47Detectors = false) {
4934
5110
  return (useV47Detectors ? ENV_TEMPLATE_PATTERN_V47 : ENV_TEMPLATE_PATTERN).test(path);
4935
5111
  }
@@ -4946,32 +5122,34 @@ function readGitTrackedFileOutput(repoPath) {
4946
5122
  if (workTree.stdout.trim() !== "true") return { ok: false, reason: "not_a_repo" };
4947
5123
  return execGit(["ls-files", "--cached"], { cwd: repoPath });
4948
5124
  }
4949
- function parseGitTrackedFiles(repoPath, output, includeHardExcluded) {
5125
+ function parseGitTrackedFiles(repoPath, output, includeHardExcluded, registerWithheld = false) {
4950
5126
  const files = [];
5127
+ const withhold = (file, reason) => {
5128
+ if (registerWithheld) registerWithheldRepoPath(file, reason);
5129
+ };
4951
5130
  for (const file of output.trim().split("\n")) {
4952
5131
  if (file.length === 0) continue;
4953
5132
  const fullPath = join(repoPath, file);
4954
5133
  if (!includeHardExcluded && isHardExcludedPath(file)) {
4955
5134
  recordContentSkip(fullPath, "denied_path", true);
5135
+ withhold(file, "denied_path");
4956
5136
  continue;
4957
5137
  }
4958
5138
  try {
4959
5139
  const stat = lstatSync(fullPath);
4960
5140
  if (!stat.isFile()) {
4961
5141
  recordContentSkip(fullPath, "non_regular_file", true);
5142
+ withhold(file, "non_regular_file");
4962
5143
  continue;
4963
5144
  }
4964
5145
  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
- }
5146
+ recordContentSkip(fullPath, "too_large", true);
5147
+ withhold(file, "too_large");
4971
5148
  continue;
4972
5149
  }
4973
5150
  } catch (error) {
4974
5151
  recordFilesystemSkip(fullPath, error, false, true);
5152
+ withhold(file, "unreadable");
4975
5153
  continue;
4976
5154
  }
4977
5155
  if (!isPotentialContentPath(file)) {
@@ -4981,39 +5159,6 @@ function parseGitTrackedFiles(repoPath, output, includeHardExcluded) {
4981
5159
  }
4982
5160
  return files.sort();
4983
5161
  }
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
5162
  function directoryInventory(repoPath) {
5018
5163
  const files = [];
5019
5164
  const rootDevice = statSync(repoPath).dev;
@@ -5024,7 +5169,7 @@ function collectRepoFileInventory(repoPath) {
5024
5169
  const tracked = readGitTrackedFileOutput(repoPath);
5025
5170
  if (tracked.ok) {
5026
5171
  return {
5027
- repoFiles: parseGitTrackedFiles(repoPath, tracked.stdout, false),
5172
+ repoFiles: parseGitTrackedFiles(repoPath, tracked.stdout, false, true),
5028
5173
  inventoryFiles: parseGitTrackedFiles(repoPath, tracked.stdout, true),
5029
5174
  repoFileInventoryScope: "tracked-scan-eligible",
5030
5175
  scanLimitations: []
@@ -5122,8 +5267,10 @@ function visitRepoDir(repoPath, dirPath, files, rootDevice) {
5122
5267
  visitRepoDir(repoPath, fullPath, files, rootDevice);
5123
5268
  continue;
5124
5269
  }
5270
+ const repoRelativePath = relative(repoPath, fullPath);
5125
5271
  if (!entry.isFile()) {
5126
5272
  recordContentSkip(fullPath, "non_regular_file");
5273
+ registerWithheldRepoPath(repoRelativePath, "non_regular_file");
5127
5274
  continue;
5128
5275
  }
5129
5276
  let size;
@@ -5131,16 +5278,12 @@ function visitRepoDir(repoPath, dirPath, files, rootDevice) {
5131
5278
  size = statSync(fullPath).size;
5132
5279
  } catch (error) {
5133
5280
  recordFilesystemSkip(fullPath, error);
5281
+ registerWithheldRepoPath(repoRelativePath, "unreadable");
5134
5282
  continue;
5135
5283
  }
5136
- const repoRelativePath = relative(repoPath, fullPath);
5137
5284
  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
- }
5285
+ recordContentSkip(fullPath, "too_large");
5286
+ registerWithheldRepoPath(repoRelativePath, "too_large");
5144
5287
  continue;
5145
5288
  }
5146
5289
  if (!isPotentialContentPath(repoRelativePath)) {
@@ -5264,6 +5407,9 @@ function measureNonHollowTestShare(repoPath, files, useV17Detectors = false) {
5264
5407
  function countFilesContaining(repoPath, files, pattern) {
5265
5408
  return files.filter((file) => fileContains(repoPath, file, pattern)).length;
5266
5409
  }
5410
+ function countFilesMatchingAny(repoPath, files, patterns) {
5411
+ return files.filter((file) => patterns.some((pattern) => fileContains(repoPath, file, pattern))).length;
5412
+ }
5267
5413
  function countPatternMatches(repoPath, files, pattern) {
5268
5414
  const flags = pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`;
5269
5415
  const matcher = new RegExp(pattern.source, flags);
@@ -5803,22 +5949,33 @@ function isDocumentationOnlyDeployWorkflow(repoPath, file) {
5803
5949
  function findPackagedApplicationPremiseFiles(repoPath, repoFiles) {
5804
5950
  return repoFiles.filter((file) => /(^|\/)package\.json$/.test(file) && isAuthoredProductionPath(file)).filter((file) => packageJsonDescribesPackagedApplication(repoPath, file, repoFiles));
5805
5951
  }
5952
+ function isClaimSourceFile(file) {
5953
+ return /^(?:README|readme)\.md$/.test(file) || /^docs\/[^/]+\.(?:md|mdx)$/.test(file);
5954
+ }
5955
+ function isClaimRealityReconciliationPath(file) {
5956
+ return /(^|\/)claim[-_]reality[-_]reconciliation\.(?:md|mdx|json)$/i.test(file);
5957
+ }
5958
+ function isSecurityOrThreatModelDoc(file) {
5959
+ return /(^|\/)(SECURITY|THREAT[_-]?MODEL)\.md$/i.test(file);
5960
+ }
5961
+ function isNegativeSpaceDocCandidate(file) {
5962
+ return isClaimSourceFile(file) || isSecurityOrThreatModelDoc(file);
5963
+ }
5964
+ function claimImplementationFileReads(authoredOnly) {
5965
+ return (file) => isImplementationFile(file) && (!authoredOnly || isAuthoredProductionPath(file));
5966
+ }
5806
5967
  function findClaimSourceFiles(repoFiles) {
5807
- return repoFiles.filter(
5808
- (file) => /^(?:README|readme)\.md$/.test(file) || /^docs\/[^/]+\.(?:md|mdx)$/.test(file)
5809
- ).sort((left, right) => {
5968
+ return repoFiles.filter(isClaimSourceFile).sort((left, right) => {
5810
5969
  const leftRootReadme = /^(?:README|readme)\.md$/.test(left);
5811
5970
  const rightRootReadme = /^(?:README|readme)\.md$/.test(right);
5812
5971
  return Number(rightRootReadme) - Number(leftRootReadme) || left.localeCompare(right);
5813
5972
  });
5814
5973
  }
5815
5974
  function findClaimImplementationFiles(repoFiles, authoredOnly = false) {
5816
- return repoFiles.filter(
5817
- (file) => isImplementationFile(file) && (!authoredOnly || isAuthoredProductionPath(file))
5818
- );
5975
+ return repoFiles.filter(claimImplementationFileReads(authoredOnly));
5819
5976
  }
5820
5977
  function findClaimRealityReconciliationArtifacts(repoPath, repoFiles) {
5821
- return repoFiles.filter((file) => /(^|\/)claim[-_]reality[-_]reconciliation\.(?:md|mdx|json)$/i.test(file)).filter((file) => isAuthenticatedClaimRealityArtifact(repoPath, file));
5978
+ return repoFiles.filter(isClaimRealityReconciliationPath).filter((file) => isAuthenticatedClaimRealityArtifact(repoPath, file));
5822
5979
  }
5823
5980
  function isAuthenticatedClaimRealityArtifact(repoPath, file) {
5824
5981
  const fullPath = join(repoPath, file);
@@ -6874,7 +7031,9 @@ var MAIN_ENTRYPOINT_FILE_PATTERN = /(^|\/)(main|server|app|wsgi|asgi|index|entry
6874
7031
  var RACK_ENTRYPOINT_FILE_PATTERN = /(^|\/)(?:config\.ru|(?:main|server|app|start|run)\.rb)$/i;
6875
7032
  var SERVER_ENTRYPOINT_PATTERN = /\bapp\.listen\s*\(|\bserver\.listen\s*\(\s*(?:PORT|port|\d+)|http\.ListenAndServe\s*\(|http\.ListenAndServeTLS\s*\(|axum::Server(?:::|\.)|actix_web::HttpServer(?:::|\.)|uvicorn\.run\s*\(/;
6876
7033
  var V20_DIRECT_HTTP_SERVER_PATTERN = /\b(?:http|https)\.createServer\s*\([\s\S]{0,1500}?\)\.listen\s*\(\s*(?:PORT|port|\d+)/;
6877
- var V20_HEALTH_OR_READINESS_ROUTE_PATTERN = /["'`]\/(?:health|ready|readiness|live|liveness)["'`]/i;
7034
+ var V20_HEALTH_OR_READINESS_ROUTE_PATTERN = /["'`][\w${}./-]{0,60}\/(?:(?:health|ready|live)z?|readiness|liveness)(?=["'`/])/i;
7035
+ var EXPRESS_ERROR_MIDDLEWARE_PATTERN = /\(\s*(?:err|error)(?:\s*:\s*(?:[^,()<]|<[^<>]*>)*)?\s*,\s*(?:req|request)(?:\s*:\s*(?:[^,()<]|<[^<>]*>)*)?\s*,\s*(?:res|response)(?:\s*:\s*(?:[^,()<]|<[^<>]*>)*)?\s*,\s*next(?:\s*:\s*(?:[^,()<]|<[^<>]*>)*)?\s*\)\s*(?:=>|\{)/i;
7036
+ var EXPRESS_MIDDLEWARE_REACHABLE_PATTERN = /\.use\s*\(|\bmodule\.exports\b|\bexport\s+(?:default\b|function\b|const\b)/;
6878
7037
  var RACK_SERVER_ENTRYPOINT_PATTERN = /Rack::(?:Server|Handler(?:::\w+)?)\.(?:start|run)\s*\(/;
6879
7038
  var RACK_CONFIG_RUN_PATTERN = /^\s*run\s+(?:(?:[A-Z]\w*(?:::\w+)*(?:\.new)?|lambda)\b|->)/m;
6880
7039
  var RUNTIME_CONTAINER_COMMAND_PATTERN = /^\s*(?:CMD|ENTRYPOINT)\s+.*(?:\b(?:start|serve|server|qgis|nginx|apache|gunicorn|uvicorn)\b|manage\.py\s+runserver).*$/im;
@@ -7066,7 +7225,69 @@ function fileContains(repoPath, file, pattern) {
7066
7225
  if (!isRegularFile(fullPath)) return false;
7067
7226
  return pattern.test(readRepoText(fullPath, "utf8"));
7068
7227
  }
7069
- function findCommittedSecretInFile(repoPath, file, useV36Detectors = false, useV39Detectors = false, useV47Detectors = false) {
7228
+ 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;
7229
+ var BARE_PEM_PRIVATE_KEY_PATTERN = /["'`]?-----BEGIN((?: (?:RSA|DSA|EC|OPENSSH|ENCRYPTED))? PRIVATE KEY)-----([\s\S]{0,32768}?)-----END\1-----/g;
7230
+ var BARE_PEM_KEY_FILE_PATTERN = /(?:^|\/)id_(?:rsa|dsa|ecdsa|ed25519)$|\.(?:pem|key)$/i;
7231
+ function isBarePemKeyFile(file) {
7232
+ return BARE_PEM_KEY_FILE_PATTERN.test(file);
7233
+ }
7234
+ 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;
7235
+ 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;
7236
+ function isPlausiblePemKeyBody(rawBody) {
7237
+ const normalized = rawBody.replace(/\\[nr]|[\r\n\s]/g, "");
7238
+ if (normalized.length < 40) return false;
7239
+ if (!/^[A-Za-z0-9+/=]+$/.test(normalized)) return false;
7240
+ return !isPlaceholderSecretValue(normalized);
7241
+ }
7242
+ function scanForPemPrivateKeyMatch(pattern, contents, lines, hasIdentifierGroup) {
7243
+ pattern.lastIndex = 0;
7244
+ let match;
7245
+ while ((match = pattern.exec(contents)) !== null) {
7246
+ const full = match[0];
7247
+ const identifier = hasIdentifierGroup ? match[1] ?? "" : "";
7248
+ const body = (hasIdentifierGroup ? match[3] : match[2]) ?? "";
7249
+ const line = contents.slice(0, match.index).split("\n").length;
7250
+ const contextLines = lines.slice(Math.max(0, line - 5), line + 4).join("\n");
7251
+ const isDevOrSelfSigned = DEV_OR_SELF_SIGNED_KEY_CONTEXT_PATTERN.test(
7252
+ `${identifier}
7253
+ ${contextLines}`
7254
+ );
7255
+ const isRotatedOrRevoked = ROTATED_OR_REVOKED_KEY_CONTEXT_PATTERN.test(contextLines);
7256
+ if (isPlausiblePemKeyBody(body) && !isDevOrSelfSigned && !isRotatedOrRevoked) {
7257
+ const value = full.slice(full.indexOf("-----BEGIN"));
7258
+ const characterClasses = [
7259
+ /[a-z]/.test(value) ? "lower" : "",
7260
+ /[A-Z]/.test(value) ? "upper" : "",
7261
+ /\d/.test(value) ? "digit" : "",
7262
+ /[^A-Za-z0-9]/.test(value) ? "symbol" : ""
7263
+ ].filter(Boolean).join("+");
7264
+ return {
7265
+ identifier,
7266
+ line,
7267
+ valueLength: value.length,
7268
+ characterClasses: characterClasses || "other",
7269
+ valueFingerprint: createHash2("sha256").update(value).digest("hex"),
7270
+ kind: "pem_private_key"
7271
+ };
7272
+ }
7273
+ }
7274
+ return null;
7275
+ }
7276
+ function findPemPrivateKeyAssignment(contents, file) {
7277
+ if (!contents.includes("-----BEGIN")) return null;
7278
+ if (DEV_OR_SELF_SIGNED_KEY_CONTEXT_PATTERN.test(file)) return null;
7279
+ const lines = contents.split("\n");
7280
+ const assignmentMatch = scanForPemPrivateKeyMatch(
7281
+ PEM_PRIVATE_KEY_ASSIGNMENT_PATTERN,
7282
+ contents,
7283
+ lines,
7284
+ true
7285
+ );
7286
+ if (assignmentMatch) return assignmentMatch;
7287
+ if (!isBarePemKeyFile(file)) return null;
7288
+ return scanForPemPrivateKeyMatch(BARE_PEM_PRIVATE_KEY_PATTERN, contents, lines, false);
7289
+ }
7290
+ function findCommittedSecretInFile(repoPath, file, useV36Detectors = false, useV39Detectors = false, useV47Detectors = false, useV23PemPrivateKeyGrammar = false) {
7070
7291
  const fullPath = join(repoPath, file);
7071
7292
  if (!isRegularFile(fullPath)) return null;
7072
7293
  const contents = readRepoText(fullPath, "utf8");
@@ -7082,6 +7303,10 @@ function findCommittedSecretInFile(repoPath, file, useV36Detectors = false, useV
7082
7303
  allowCredentialNamedDigest: useV36Detectors
7083
7304
  });
7084
7305
  if (secret) return secret;
7306
+ if (useV23PemPrivateKeyGrammar) {
7307
+ const pemSecret = findPemPrivateKeyAssignment(secretScanContents, file);
7308
+ if (pemSecret) return pemSecret;
7309
+ }
7085
7310
  if (useV39Detectors && isEnvHistoryPath(file, useV36Detectors)) {
7086
7311
  const weakCredential = findWeakExplicitEnvCredential(secretScanContents);
7087
7312
  if (weakCredential) return weakCredential;
@@ -7174,7 +7399,7 @@ function hasTemplateOnlyEnvContent(repoPath, file) {
7174
7399
  return value.length === 0 || isPlaceholderSecretValue(value);
7175
7400
  });
7176
7401
  }
7177
- function findSecretFingerprintsInFile(repoPath, file, useV36Detectors = false, useV39Detectors = false, useV47Detectors = false) {
7402
+ function findSecretFingerprintsInFile(repoPath, file, useV36Detectors = false, useV39Detectors = false, useV47Detectors = false, useV23PemPrivateKeyGrammar = false) {
7178
7403
  const fullPath = join(repoPath, file);
7179
7404
  if (!isRegularFile(fullPath)) return [];
7180
7405
  const contents = readRepoText(fullPath, "utf8");
@@ -7191,6 +7416,10 @@ function findSecretFingerprintsInFile(repoPath, file, useV36Detectors = false, u
7191
7416
  Number.POSITIVE_INFINITY,
7192
7417
  { allowCredentialNamedDigest: useV36Detectors }
7193
7418
  ).map(({ valueFingerprint }) => valueFingerprint);
7419
+ if (useV23PemPrivateKeyGrammar) {
7420
+ const pemSecret = findPemPrivateKeyAssignment(scanContents, file);
7421
+ if (pemSecret) fingerprints.push(pemSecret.valueFingerprint);
7422
+ }
7194
7423
  if (useV39Detectors && isEnvHistoryPath(file, useV36Detectors)) {
7195
7424
  const weakCredential = findWeakExplicitEnvCredential(scanContents);
7196
7425
  if (weakCredential) fingerprints.push(weakCredential.valueFingerprint);
@@ -7334,24 +7563,32 @@ function declareHistoryFailure(scanLimitations, reason, operation, maxBufferByte
7334
7563
  `${describeGitFailure(reason, operation, maxBufferBytes)} Cejel continued with reduced Git history evidence.`
7335
7564
  );
7336
7565
  }
7337
- function collectHistorySecretEvidence(repoPath, excludeHead, currentSecretFingerprintsByPath, scanLimitations, authoredProductionOnly = false, useV36Detectors = false, useV39Detectors = false, useV47Detectors = false) {
7566
+ function collectHistorySecretEvidence(repoPath, excludeHead, currentSecretFingerprintsByPath, scanLimitations, authoredProductionOnly = false, useV36Detectors = false, useV39Detectors = false, useV47Detectors = false, useV23PemPrivateKeyGrammar = false) {
7338
7567
  const historyEntries = readCredentialHistoryEntries(
7339
7568
  repoPath,
7340
7569
  scanLimitations,
7341
- useV36Detectors
7570
+ useV36Detectors,
7571
+ useV23PemPrivateKeyGrammar
7342
7572
  );
7343
7573
  const head = excludeHead ? readGitHead(repoPath) : null;
7344
7574
  let scannedFiles = 0;
7575
+ let v23WidenedOnlyScanned = 0;
7345
7576
  let envPathEvidence = null;
7346
7577
  for (const entry of historyEntries) {
7347
7578
  if (head && entry.commit === head) continue;
7348
7579
  if (authoredProductionOnly && !(useV47Detectors ? isV47AuthoredProductionPath(entry.path) : useV39Detectors ? isV39AuthoredProductionPath(entry.path) : isAuthoredProductionPath(entry.path))) {
7349
7580
  continue;
7350
7581
  }
7351
- if (scannedFiles >= HISTORY_SECRET_SCAN_CREDENTIAL_BLOB_LIMIT) {
7352
- return { evidence: null, envPathEvidence, truncated: true };
7582
+ const isV23WidenedOnly = useV23PemPrivateKeyGrammar && isV23PemCredentialHistoryPath(historyPathBasename(entry.path.toLowerCase())) && !isCredentialHistoryPath(entry.path, useV36Detectors, false);
7583
+ if (isV23WidenedOnly) {
7584
+ if (v23WidenedOnlyScanned >= V23_PEM_WIDENED_HISTORY_BLOB_LIMIT) continue;
7585
+ v23WidenedOnlyScanned += 1;
7586
+ } else {
7587
+ if (scannedFiles >= HISTORY_SECRET_SCAN_CREDENTIAL_BLOB_LIMIT) {
7588
+ return { evidence: null, secretKind: null, envPathEvidence, truncated: true };
7589
+ }
7590
+ scannedFiles += 1;
7353
7591
  }
7354
- scannedFiles += 1;
7355
7592
  const contents = readGitBlob(repoPath, entry.commit, entry.path, scanLimitations);
7356
7593
  if (!contents) continue;
7357
7594
  const scanContents = prepareCredentialScanContents(
@@ -7365,6 +7602,12 @@ function collectHistorySecretEvidence(repoPath, excludeHead, currentSecretFinger
7365
7602
  let secretMatch = findRealSecretAssignment(scanContents, currentFingerprints, {
7366
7603
  allowCredentialNamedDigest: useV36Detectors
7367
7604
  });
7605
+ if (!secretMatch && useV23PemPrivateKeyGrammar) {
7606
+ const pemMatch = findPemPrivateKeyAssignment(scanContents, entry.path);
7607
+ if (pemMatch && !currentFingerprints.has(pemMatch.valueFingerprint)) {
7608
+ secretMatch = pemMatch;
7609
+ }
7610
+ }
7368
7611
  if (!secretMatch && useV39Detectors && isEnvHistoryPath(entry.path, useV36Detectors, useV47Detectors)) {
7369
7612
  const weakCredential = findWeakExplicitEnvCredential(scanContents);
7370
7613
  if (weakCredential && !currentFingerprints.has(weakCredential.valueFingerprint)) {
@@ -7375,11 +7618,15 @@ function collectHistorySecretEvidence(repoPath, excludeHead, currentSecretFinger
7375
7618
  return {
7376
7619
  evidence: {
7377
7620
  kind: "secret_scan",
7378
- label: secretEvidenceLabel("Committed secret in recent git history", secretMatch),
7621
+ label: secretEvidenceLabel(
7622
+ secretMatch.kind === "pem_private_key" ? "Committed PEM-formatted private key in recent git history" : "Committed secret in recent git history",
7623
+ secretMatch
7624
+ ),
7379
7625
  path: entry.path,
7380
7626
  line: secretMatch.line,
7381
7627
  contentHash: entry.commit
7382
7628
  },
7629
+ secretKind: secretMatch.kind ?? null,
7383
7630
  envPathEvidence,
7384
7631
  truncated: false
7385
7632
  };
@@ -7393,9 +7640,9 @@ function collectHistorySecretEvidence(repoPath, excludeHead, currentSecretFinger
7393
7640
  };
7394
7641
  }
7395
7642
  }
7396
- return { evidence: null, envPathEvidence, truncated: false };
7643
+ return { evidence: null, secretKind: null, envPathEvidence, truncated: false };
7397
7644
  }
7398
- function readCredentialHistoryEntries(repoPath, scanLimitations, useV36Detectors = false) {
7645
+ function readCredentialHistoryEntries(repoPath, scanLimitations, useV36Detectors = false, useV23PemPrivateKeyGrammar = false) {
7399
7646
  const entries = [];
7400
7647
  const seen = /* @__PURE__ */ new Set();
7401
7648
  const appendEntry = (entry) => {
@@ -7440,7 +7687,7 @@ function readCredentialHistoryEntries(repoPath, scanLimitations, useV36Detectors
7440
7687
  scanLimitations.add(CONTROL_CHARACTER_HISTORY_PATH_LIMITATION);
7441
7688
  continue;
7442
7689
  }
7443
- if (!isCredentialHistoryPath(path, useV36Detectors)) continue;
7690
+ if (!isCredentialHistoryPath(path, useV36Detectors, useV23PemPrivateKeyGrammar)) continue;
7444
7691
  appendEntry({ commit: currentCommit, path });
7445
7692
  }
7446
7693
  }
@@ -7448,17 +7695,19 @@ function readCredentialHistoryEntries(repoPath, scanLimitations, useV36Detectors
7448
7695
  for (const entry of readDeletedCredentialHistoryEntries(
7449
7696
  repoPath,
7450
7697
  scanLimitations,
7451
- useV36Detectors
7698
+ useV36Detectors,
7699
+ useV23PemPrivateKeyGrammar
7452
7700
  )) {
7453
7701
  appendEntry(entry);
7454
7702
  }
7455
7703
  return entries;
7456
7704
  }
7457
- function readDeletedCredentialHistoryEntries(repoPath, scanLimitations, useV36Detectors = false) {
7705
+ function readDeletedCredentialHistoryEntries(repoPath, scanLimitations, useV36Detectors = false, useV23PemPrivateKeyGrammar = false) {
7458
7706
  const deletedPaths = readDeletedCredentialHistoryPaths(
7459
7707
  repoPath,
7460
7708
  scanLimitations,
7461
- useV36Detectors
7709
+ useV36Detectors,
7710
+ useV23PemPrivateKeyGrammar
7462
7711
  );
7463
7712
  const entries = [];
7464
7713
  const seen = /* @__PURE__ */ new Set();
@@ -7472,7 +7721,7 @@ function readDeletedCredentialHistoryEntries(repoPath, scanLimitations, useV36De
7472
7721
  }
7473
7722
  return entries;
7474
7723
  }
7475
- function readDeletedCredentialHistoryPaths(repoPath, scanLimitations, useV36Detectors = false) {
7724
+ function readDeletedCredentialHistoryPaths(repoPath, scanLimitations, useV36Detectors = false, useV23PemPrivateKeyGrammar = false) {
7476
7725
  const historyResult = execGit(
7477
7726
  [
7478
7727
  "log",
@@ -7500,7 +7749,7 @@ function readDeletedCredentialHistoryPaths(repoPath, scanLimitations, useV36Dete
7500
7749
  continue;
7501
7750
  }
7502
7751
  if (!path || seen.has(path)) continue;
7503
- if (!isCredentialHistoryPath(path, useV36Detectors)) continue;
7752
+ if (!isCredentialHistoryPath(path, useV36Detectors, useV23PemPrivateKeyGrammar)) continue;
7504
7753
  seen.add(path);
7505
7754
  paths.push(path);
7506
7755
  }
@@ -7518,14 +7767,24 @@ function readHistoryCommitsForPath(repoPath, path, scanLimitations) {
7518
7767
  }
7519
7768
  return historyResult.stdout.split("\n").map((line) => line.trim()).filter((line) => /^[0-9a-f]{40}$/i.test(line));
7520
7769
  }
7521
- function isCredentialHistoryPath(path, _useV36Detectors = false) {
7770
+ var V23_KEY_SHAPED_JSON_PATH_PATTERN = /(?<![A-Za-z0-9])(?:service[-_]?account|private[-_]?key|creds?|key)(?![A-Za-z0-9])/i;
7771
+ function historyPathBasename(lowerPath) {
7772
+ return lowerPath.split("/").at(-1) ?? lowerPath;
7773
+ }
7774
+ function isV23PemCredentialHistoryPath(basename3) {
7775
+ return basename3.endsWith(".json") && V23_KEY_SHAPED_JSON_PATH_PATTERN.test(basename3);
7776
+ }
7777
+ function isCredentialHistoryPath(path, _useV36Detectors = false, useV23PemPrivateKeyGrammar = false) {
7522
7778
  if (isHardExcludedPath(path)) return false;
7523
7779
  const lowerPath = path.toLowerCase();
7524
- const basename3 = lowerPath.split("/").at(-1) ?? lowerPath;
7780
+ const basename3 = historyPathBasename(lowerPath);
7525
7781
  if (/\.env(?:\.|$)/.test(basename3)) {
7526
7782
  return true;
7527
7783
  }
7528
- return lowerPath.includes("secret") || lowerPath.includes("credential") || basename3.endsWith(".pem") || basename3.endsWith(".key") || basename3 === "id_rsa" || basename3.endsWith(".p12") || basename3.endsWith(".pfx");
7784
+ if (lowerPath.includes("secret") || lowerPath.includes("credential") || basename3.endsWith(".pem") || basename3.endsWith(".key") || basename3 === "id_rsa" || basename3.endsWith(".p12") || basename3.endsWith(".pfx")) {
7785
+ return true;
7786
+ }
7787
+ return useV23PemPrivateKeyGrammar && isV23PemCredentialHistoryPath(basename3);
7529
7788
  }
7530
7789
  function isEnvHistoryPath(path, useV36Detectors = false, useV47Detectors = false) {
7531
7790
  if (isEnvTemplatePath(path, useV47Detectors)) return false;
@@ -8235,6 +8494,22 @@ function discoverIngestInputs(repoPath, maxMatches = Number.POSITIVE_INFINITY) {
8235
8494
  // src/witan/scoring.ts
8236
8495
  init_esm_shims();
8237
8496
 
8497
+ // src/witan/rubric-fingerprint.ts
8498
+ init_esm_shims();
8499
+ import { createHash as createHash3 } from "crypto";
8500
+ var WITAN_RUBRIC_BEHAVIOUR_FINGERPRINTS = Object.freeze({
8501
+ [WITAN_RUBRIC_VERSION_V17]: "sha256:ff0f01abe8c12daa60375d0e18c1aca4a1135a55ab2a80b15f4c037bfc18b4b1",
8502
+ [WITAN_RUBRIC_VERSION_V18]: "sha256:abc35df0fdc0749aa941f47b295059d06c96dd9800af7f79cfef2bc8dae917da",
8503
+ [WITAN_RUBRIC_VERSION_V19]: "sha256:f670fa2cc44beef066fa38767bd587e1a16fb28b03bc904756daaea5a6465e96",
8504
+ [WITAN_RUBRIC_VERSION_V20]: "sha256:9276ce2ce865c2d50c52882ee45bbf1df5fe3c8b6038858b4a147f7a4cedfa98",
8505
+ [WITAN_RUBRIC_VERSION_V21]: "sha256:d86411a7fe108bba4d2b8fe7ff9d4f7f69a60f11ca0c5c3fef8e88f66a9af53a",
8506
+ [WITAN_RUBRIC_VERSION_V22]: "sha256:1bb57dd98d29b43b170f3a4f1c8b3b379b433a2aa9df0821ab9d5115f48cfb2f",
8507
+ [WITAN_RUBRIC_VERSION_V23]: "sha256:b558c0aa4b2f85b734bed2e077298fd55851a1086234d678f10a71cf913ec3cf"
8508
+ });
8509
+ function rubricBehaviourFingerprint(rubricVersion) {
8510
+ return WITAN_RUBRIC_BEHAVIOUR_FINGERPRINTS[rubricVersion];
8511
+ }
8512
+
8238
8513
  // src/witan/rubric.ts
8239
8514
  init_esm_shims();
8240
8515
  var WITAN_RUBRIC = [
@@ -8300,11 +8575,11 @@ function usesV17DetectorClosure2(rubricVersion) {
8300
8575
  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
8576
  }
8302
8577
  var MAX_SIGNAL_ADJUSTMENT = 0.8;
8303
- function createWitanReport(input, inputSignals, rubric = WITAN_RUBRIC) {
8578
+ function createWitanReport(input, inputSignals, rubric = WITAN_RUBRIC, toolVersion) {
8304
8579
  const parsedInput = WitanReportInputSchema.parse(input);
8305
8580
  const signalsByCriterion = mergeSignalsByCriterion(parsedInput.signals);
8306
8581
  const readFailureAbstentions = new Set(
8307
- [...signalsByCriterion.values()].filter((signal) => signal.insufficientData === true).map((signal) => signal.criterionId)
8582
+ [...signalsByCriterion.values()].filter((signal) => signal.insufficientData === true && !isSelfImposedAbstention(signal)).map((signal) => signal.criterionId)
8308
8583
  );
8309
8584
  const hasInputSignals = inputSignals != null && inputSignals.length > 0;
8310
8585
  const inputSignalsByDimension = /* @__PURE__ */ new Map();
@@ -8394,6 +8669,13 @@ function createWitanReport(input, inputSignals, rubric = WITAN_RUBRIC) {
8394
8669
  productDisplayName: parsedInput.productDisplayName,
8395
8670
  repo: parsedInput.repo,
8396
8671
  rubricVersion: parsedInput.rubricVersion,
8672
+ // Behavioural identity beside the rubric's name, so a reader holding two certificates can
8673
+ // check whether the scoring that produced them agrees instead of inferring it from a string
8674
+ // the tool's author controls. Derived only from parsedInput.rubricVersion, so it is a
8675
+ // property of the (build, rubric) pair and not of this invocation: two runs of the same
8676
+ // installed version at the same revision stay byte-identical. Absent, never fabricated, for
8677
+ // a rubric this build carries no measurement for.
8678
+ ...rubricBehaviourFingerprint(parsedInput.rubricVersion) ? { rubricBehaviourFingerprint: rubricBehaviourFingerprint(parsedInput.rubricVersion) } : {},
8397
8679
  verdict: abstained ? "insufficient_source" : witanVerdictForScore(overallScore),
8398
8680
  codeTrustScore: abstained ? null : codeTrustScore,
8399
8681
  processTrustScore: abstained ? null : processTrustScore,
@@ -8409,7 +8691,8 @@ function createWitanReport(input, inputSignals, rubric = WITAN_RUBRIC) {
8409
8691
  // alter it (see classifyRepoArchetype in repo-signals.ts); per-criterion scoring above is
8410
8692
  // unaffected by archetype, only presentation layers (badge/terminal/verdict) key off it.
8411
8693
  ...parsedInput.archetype ? { archetype: parsedInput.archetype } : {},
8412
- ...insufficientSourceReason ? { insufficientSourceReason } : {}
8694
+ ...insufficientSourceReason ? { insufficientSourceReason } : {},
8695
+ ...toolVersion ? { toolVersion } : {}
8413
8696
  });
8414
8697
  }
8415
8698
  function applySignalAdjustment(nativeScore, dimension, signals) {
@@ -8479,6 +8762,14 @@ function statusAfterInputAdjustment(nativeStatus, nativeScore, adjustment, rubri
8479
8762
  );
8480
8763
  return (severity[adjustedSemanticStatus] ?? nativeSeverity) > nativeSeverity ? adjustedSemanticStatus : nativeStatus;
8481
8764
  }
8765
+ var SELF_IMPOSED_ABSTENTION_REASONS = /* @__PURE__ */ new Set([
8766
+ "too_large",
8767
+ "excluded_by_extension",
8768
+ "non_regular_file"
8769
+ ]);
8770
+ function isSelfImposedAbstention(signal) {
8771
+ return signal.insufficientDataReason !== void 0 && SELF_IMPOSED_ABSTENTION_REASONS.has(signal.insufficientDataReason);
8772
+ }
8482
8773
  function mergeSignalsByCriterion(signals) {
8483
8774
  const merged = /* @__PURE__ */ new Map();
8484
8775
  for (const signal of signals) {
@@ -8487,15 +8778,19 @@ function mergeSignalsByCriterion(signals) {
8487
8778
  merged.set(signal.criterionId, signal);
8488
8779
  continue;
8489
8780
  }
8781
+ const existingAbstains = existing.insufficientData === true;
8782
+ const signalAbstains = signal.insufficientData === true;
8783
+ const abstentionReason = !existingAbstains && !signalAbstains ? void 0 : [existing, signal].filter((candidate) => candidate.insufficientData === true).some((candidate) => !isSelfImposedAbstention(candidate)) ? void 0 : signal.insufficientDataReason ?? existing.insufficientDataReason;
8490
8784
  merged.set(signal.criterionId, {
8491
8785
  criterionId: signal.criterionId,
8492
8786
  positiveEvidence: [...existing.positiveEvidence, ...signal.positiveEvidence],
8493
8787
  findings: [...existing.findings, ...signal.findings],
8494
8788
  metrics: [...existing.metrics ?? [], ...signal.metrics ?? []],
8495
8789
  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 } : {}
8790
+ ...existingAbstains || signalAbstains ? {
8791
+ insufficientData: true,
8792
+ ...abstentionReason !== void 0 ? { insufficientDataReason: abstentionReason } : {}
8793
+ } : existing.notApplicable === true || signal.notApplicable === true ? { notApplicable: true } : {}
8499
8794
  });
8500
8795
  }
8501
8796
  return merged;
@@ -8931,7 +9226,12 @@ function scoreRepoWithPublicCejel(options) {
8931
9226
  ...options.rubricVersion ? { rubricVersion: options.rubricVersion } : {}
8932
9227
  });
8933
9228
  const inputSignals = resolvePublicIngestSignals(options);
8934
- const report = createWitanReport(input, inputSignals.length > 0 ? inputSignals : void 0);
9229
+ const report = createWitanReport(
9230
+ input,
9231
+ inputSignals.length > 0 ? inputSignals : void 0,
9232
+ void 0,
9233
+ options.toolVersion
9234
+ );
8935
9235
  if (report.verdict !== "insufficient_source" || !isWitanNoMeasurementAbstention(report)) {
8936
9236
  return report;
8937
9237
  }
@@ -9189,7 +9489,8 @@ function runCejelScan(options) {
9189
9489
  // the repository. Direct public/leaderboard scans default this off because their targets
9190
9490
  // are third-party repositories and must opt in explicitly.
9191
9491
  autoDiscoverIngest: true,
9192
- warnOnEmptyIngestMatch: options.warnOnEmptyIngestMatch
9492
+ warnOnEmptyIngestMatch: options.warnOnEmptyIngestMatch,
9493
+ ...options.toolVersion ? { toolVersion: options.toolVersion } : {}
9193
9494
  });
9194
9495
  const summary = buildWitanCliSummary(report);
9195
9496
  return { report, summary, generatedAt };