@cejel/cejel 0.4.6 → 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.
@@ -5,21 +5,24 @@ import {
5
5
  describeGitFailure,
6
6
  execGit,
7
7
  isExpectedGitAbsence
8
- } from "./chunk-BZXBLDCJ.js";
8
+ } from "./chunk-KNQBI3IX.js";
9
9
  import {
10
10
  external_exports
11
- } from "./chunk-Y6NO5HAI.js";
11
+ } from "./chunk-J6R5OHN7.js";
12
12
  import {
13
13
  MAX_REPOSITORY_CONTENT_BYTES,
14
+ classifyAbstentionReason,
15
+ contentReadSignalKey,
14
16
  init_esm_shims,
15
17
  readRepoText,
16
18
  readRepoTextPrefix,
17
19
  recordContentSkip,
18
- recordContentSkipForCriteria,
19
20
  recordFilesystemSkip,
20
21
  trackContentReads,
21
- withContentReadCriterion
22
- } from "./chunk-H55KECVT.js";
22
+ withContentReadCriterion,
23
+ withContentReadSignal,
24
+ withContentReadSignals
25
+ } from "./chunk-V3B2V4WA.js";
23
26
 
24
27
  // src/witan/schemas.ts
25
28
  init_esm_shims();
@@ -159,6 +162,13 @@ var WitanCriterionMetricSchema = external_exports.object({
159
162
  condition: external_exports.string().min(1).max(240).optional()
160
163
  }).strict().optional()
161
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
+ ]);
162
172
  var WitanCriterionSignalSchema = external_exports.object({
163
173
  criterionId: WitanCriterionIdSchema,
164
174
  positiveEvidence: external_exports.array(WitanEvidencePointerSchema).default([]),
@@ -168,9 +178,19 @@ var WitanCriterionSignalSchema = external_exports.object({
168
178
  notApplicable: external_exports.literal(true).optional(),
169
179
  // A collector reached relevant files but could not read their contents. This is a
170
180
  // measurement gap, never negative evidence: scoreCriterion maps it to insufficient_data.
171
- 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()
172
190
  }).strict().refine((signal) => !(signal.notApplicable && signal.insufficientData), {
173
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."
174
194
  });
175
195
  var WitanContentReadSummarySchema = external_exports.object({
176
196
  skipped: external_exports.number().int().nonnegative(),
@@ -298,7 +318,14 @@ var WitanReportCommonSchema = external_exports.object({
298
318
  consumedSignals: external_exports.array(WitanConsumedSignalSummarySchema).optional(),
299
319
  archetype: WitanRepoArchetypeSchema.optional(),
300
320
  scanLimitations: external_exports.array(external_exports.string().min(1).max(1e3)).max(16).optional(),
301
- 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()
302
329
  });
303
330
  var WitanScoredReportSchema = WitanReportCommonSchema.extend({
304
331
  verdict: WitanScoredVerdictSchema,
@@ -339,7 +366,7 @@ var WitanReportSchema = external_exports.preprocess((value) => {
339
366
  }, StrictWitanReportSchema);
340
367
  var WITAN_ATTESTATION_STATEMENT_TYPE = "https://in-toto.io/Statement/v1";
341
368
  var WITAN_ATTESTATION_PREDICATE_TYPE = "https://cejel.dev/attestations/scan/v1";
342
- var WITAN_REPORT_FORMAT_VERSION = "1.0";
369
+ var WITAN_REPORT_FORMAT_VERSION = "1.1";
343
370
  var WitanAttestationOutcomeSchema = external_exports.discriminatedUnion("status", [
344
371
  external_exports.object({
345
372
  status: external_exports.literal("scored"),
@@ -374,6 +401,11 @@ var WitanAttestationStatementSchema = external_exports.object({
374
401
  // explicit report contract version.
375
402
  reportFormatVersion: external_exports.string().regex(/^1\.\d+$/).optional(),
376
403
  rubricVersion: external_exports.string().min(1).max(120),
404
+ // Only present when the scan ran under the GitHub Action (GITHUB_RUN_ATTEMPT observed in
405
+ // the runner environment). Absent, never fabricated or defaulted to "1", for a local CLI
406
+ // or any other CI. Not part of report.json: it is per-invocation, not per-repository-
407
+ // state, and report.json's reproducibility guarantee excludes exactly that kind of field.
408
+ githubRunAttempt: external_exports.string().regex(/^[1-9][0-9]*$/).optional(),
377
409
  repository: external_exports.object({
378
410
  productSlug: external_exports.string().regex(/^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$/),
379
411
  url: external_exports.string().url().optional(),
@@ -415,13 +447,15 @@ var WITAN_RUBRIC_VERSION_V19 = "witan-rubric-v19-prospective-2026-08-09";
415
447
  var WITAN_RUBRIC_VERSION_V20 = "witan-rubric-v20-prospective-2026-08-10";
416
448
  var WITAN_RUBRIC_VERSION_V21 = "witan-rubric-v21-prospective-2026-08-10";
417
449
  var WITAN_RUBRIC_VERSION_V22 = "witan-rubric-v22-prospective-2026-08-10";
450
+ var WITAN_RUBRIC_VERSION_V23 = "witan-rubric-v23-prospective-2026-09-06";
418
451
  var WITAN_LAST_CALIBRATED_RUBRIC_VERSION = WITAN_RUBRIC_VERSION_V17;
419
452
  var WITAN_PROSPECTIVE_RUBRIC_VERSIONS = Object.freeze([
420
453
  WITAN_RUBRIC_VERSION_V18,
421
454
  WITAN_RUBRIC_VERSION_V19,
422
455
  WITAN_RUBRIC_VERSION_V20,
423
456
  WITAN_RUBRIC_VERSION_V21,
424
- WITAN_RUBRIC_VERSION_V22
457
+ WITAN_RUBRIC_VERSION_V22,
458
+ WITAN_RUBRIC_VERSION_V23
425
459
  ]);
426
460
  var WITAN_SELECTABLE_RUBRIC_VERSIONS = Object.freeze([
427
461
  WITAN_LAST_CALIBRATED_RUBRIC_VERSION,
@@ -810,6 +844,32 @@ function glossaryEntryForMetric(metric2) {
810
844
  function formatCertificateMetricLabel(metric2) {
811
845
  return CERTIFICATE_METRIC_REGISTRY[metric2.name]?.displayLabel ?? metric2.label;
812
846
  }
847
+ function formatCertificateMetricAppliedWeightPercent(criterion, metric2) {
848
+ const metrics = criterion.metrics;
849
+ const index = metrics.indexOf(metric2);
850
+ if (index === -1) {
851
+ throw new Error(
852
+ "formatCertificateMetricAppliedWeightPercent: metric does not belong to criterion.metrics"
853
+ );
854
+ }
855
+ const totalWeight = metrics.reduce((sum, m) => sum + m.weight, 0);
856
+ if (totalWeight <= 0) return 0;
857
+ const shares = metrics.map((m, i) => {
858
+ const raw = m.weight / totalWeight * 100;
859
+ const floor = Math.floor(raw);
860
+ return { i, floor, fraction: raw - floor };
861
+ });
862
+ const flooredTotal = shares.reduce((sum, s) => sum + s.floor, 0);
863
+ const byLargestRemainder = [...shares].sort((a, b) => b.fraction - a.fraction);
864
+ const result = new Map(shares.map((s) => [s.i, s.floor]));
865
+ let remainder = Math.round(100 - flooredTotal);
866
+ for (const s of byLargestRemainder) {
867
+ if (remainder <= 0) break;
868
+ result.set(s.i, (result.get(s.i) ?? s.floor) + 1);
869
+ remainder -= 1;
870
+ }
871
+ return result.get(index) ?? 0;
872
+ }
813
873
  function glossaryEntriesForReport(report) {
814
874
  const entries = new Map(CERTIFICATE_GLOSSARY.map((entry) => [entry.key, entry]));
815
875
  for (const metric2 of report.criteria.flatMap((criterion) => criterion.metrics)) {
@@ -925,6 +985,74 @@ function formatCertificateMetricValue(criterion, metric2) {
925
985
  `${formatMetricNumber(metric2.value)}/${formatMetricNumber(metric2.max)}${unit}`
926
986
  );
927
987
  }
988
+ var NEXT_STEP_FINDING_SEVERITIES = /* @__PURE__ */ new Set(["critical", "warning"]);
989
+ var NEXT_STEP_FINDING_SEVERITY_RANK = {
990
+ critical: 0,
991
+ warning: 1,
992
+ info: 2
993
+ };
994
+ var MAX_NEXT_STEP_FINDINGS = 3;
995
+ function buildNextSteps(report, unmeasured, notApplicable, a1, coverageMetric) {
996
+ const steps = [];
997
+ if (unmeasured.length > 0) {
998
+ const ids = unmeasured.map((criterion) => criterion.id).join(", ");
999
+ steps.push(
1000
+ `Obtain measurable evidence for ${ids} \u2014 ${unmeasured.length === 1 ? "this scored dimension has" : "these scored dimensions have"} no recorded result, and could change the verdict.`
1001
+ );
1002
+ }
1003
+ const criterionFindings = report.criteria.flatMap(
1004
+ (criterion) => criterion.findings.filter((finding) => NEXT_STEP_FINDING_SEVERITIES.has(finding.severity)).map((finding) => ({ criterionId: criterion.id, finding }))
1005
+ );
1006
+ const sortedFindings = [...criterionFindings].sort(
1007
+ (a, b) => NEXT_STEP_FINDING_SEVERITY_RANK[a.finding.severity] - NEXT_STEP_FINDING_SEVERITY_RANK[b.finding.severity]
1008
+ );
1009
+ for (const { criterionId, finding } of sortedFindings.slice(0, MAX_NEXT_STEP_FINDINGS)) {
1010
+ steps.push(
1011
+ `Evidence is currently absent for the ${finding.severity} finding on ${criterionId}: obtain independent verification before relying on this criterion.`
1012
+ );
1013
+ }
1014
+ if (sortedFindings.length > MAX_NEXT_STEP_FINDINGS) {
1015
+ steps.push(
1016
+ `${sortedFindings.length - MAX_NEXT_STEP_FINDINGS} more critical/warning finding(s) need the same independent verification; see the findings listed above for the complete set.`
1017
+ );
1018
+ }
1019
+ if (a1 && coverageMetric && isCoverageNotMeasured(a1, coverageMetric)) {
1020
+ steps.push(
1021
+ "Obtain a coverage report or configured threshold for A1's static coverage metric; none was found, so it was not measured."
1022
+ );
1023
+ }
1024
+ if (!report.repo.headSha && report.criteria.some(
1025
+ (criterion) => criterion.metrics.some((metric2) => metric2.name === "pr_merge_ratio")
1026
+ )) {
1027
+ steps.push(
1028
+ "Restore Git history access so the recent-commit PR-reference proxy has commit subjects to inspect; its scoring zero here is a conservative default, not a detected outcome."
1029
+ );
1030
+ }
1031
+ if (notApplicable.length > 0) {
1032
+ const ids = notApplicable.map((criterion) => criterion.id).join(", ");
1033
+ steps.push(
1034
+ `Confirm ${ids} ${notApplicable.length === 1 ? "is" : "are"} correctly marked not applicable to this repository; ${notApplicable.length === 1 ? "it was" : "they were"} not examined.`
1035
+ );
1036
+ }
1037
+ const scanLimitationCount = report.scanLimitations?.length ?? 0;
1038
+ if (scanLimitationCount > 0) {
1039
+ steps.push(
1040
+ `Resolve the ${scanLimitationCount} scan limitation${scanLimitationCount === 1 ? "" : "s"} qualifying the readable evidence, then reproduce the scan.`
1041
+ );
1042
+ }
1043
+ const skippedCount = report.contentReadSummary?.skipped ?? 0;
1044
+ if (skippedCount > 0) {
1045
+ steps.push(
1046
+ `Make the ${skippedCount} skipped content entr${skippedCount === 1 ? "y" : "ies"} readable (see the itemized reasons in the certificate), then reproduce the scan.`
1047
+ );
1048
+ }
1049
+ if (steps.length === 0) {
1050
+ steps.push(
1051
+ "No unresolved evidence gaps were identified across the categories Cejel tracks; reproduce the scan at the reported revision when one is available before relying on this certificate."
1052
+ );
1053
+ }
1054
+ return steps;
1055
+ }
928
1056
  function buildRelyingPartySummary(report) {
929
1057
  const applicable = report.criteria.filter((criterion) => criterion.status !== "not_applicable");
930
1058
  const measured = applicable.filter((criterion) => criterion.status !== "insufficient_data");
@@ -972,7 +1100,7 @@ function buildRelyingPartySummary(report) {
972
1100
  examined: `Cejel examined the repository evidence recorded for ${report.productDisplayName}${revision} under ${report.rubricVersion}.`,
973
1101
  established: `The report established measured results for ${measured.length} of ${applicable.length} applicable rubric dimensions and recorded ${findings} evidence-backed finding${findings === 1 ? "" : "s"}.`,
974
1102
  notEstablished: gaps.join(" "),
975
- next: "Review the cited evidence and open or unverified items, reproduce the scan at the reported revision when one is available, and obtain missing measurements or resolve limitations before deciding whether to accept the code."
1103
+ next: buildNextSteps(report, unmeasured, notApplicable, a1, coverageMetric).join(" ")
976
1104
  };
977
1105
  }
978
1106
  function formatMetricNumber(value) {
@@ -1091,6 +1219,7 @@ function renderWitanHtmlReport(report, options = {}) {
1091
1219
  ${options.generatedAt ? `<div><dt>Date</dt><dd>${escapeHtml(formatDate(options.generatedAt))}</dd></div>` : ""}
1092
1220
  <div><dt>Run</dt><dd>${escapeHtml(renderRepo(report))}</dd></div>
1093
1221
  <div><dt>CLI</dt><dd>${escapeHtml(options.cliVersion ? `Cejel ${options.cliVersion}` : "Not recorded")}</dd></div>
1222
+ ${options.runAttempt ? `<div><dt>Run attempt</dt><dd>${escapeHtml(options.runAttempt)}</dd></div>` : ""}
1094
1223
  <div><dt>Rubric</dt><dd>${escapeHtml(report.rubricVersion)}</dd></div>
1095
1224
  ${// Gated on the rubric actually being prospective — a calibrated (v17) certificate's
1096
1225
  // HTML stays byte-identical to before --rubric-pin existed.
@@ -1113,6 +1242,8 @@ function renderWitanHtmlReport(report, options = {}) {
1113
1242
  </div>
1114
1243
  </header>
1115
1244
 
1245
+ ${renderFindingsFirstSummary(report)}
1246
+
1116
1247
  ${renderRelyingPartySummary(relyingPartySummary)}
1117
1248
 
1118
1249
  ${(report.scanLimitations?.length ?? 0) > 0 ? `<section class="scan-limitations-section evidence-section" aria-label="Scan limitations">
@@ -1154,6 +1285,26 @@ function renderWitanHtmlReport(report, options = {}) {
1154
1285
  </html>
1155
1286
  `;
1156
1287
  }
1288
+ var FINDINGS_FIRST_SEVERITY_ORDER = {
1289
+ critical: 0,
1290
+ warning: 1,
1291
+ info: 2
1292
+ };
1293
+ function collectFindingsFirstEntries(report) {
1294
+ return report.criteria.flatMap(
1295
+ (criterion) => criterion.findings.filter((finding) => finding.severity === "critical" || finding.severity === "warning").map((finding) => ({ criterion, finding }))
1296
+ ).sort(
1297
+ (a, b) => FINDINGS_FIRST_SEVERITY_ORDER[a.finding.severity] - FINDINGS_FIRST_SEVERITY_ORDER[b.finding.severity]
1298
+ );
1299
+ }
1300
+ function renderFindingsFirstSummary(report) {
1301
+ const entries = collectFindingsFirstEntries(report);
1302
+ const body = entries.length > 0 ? `<ul>${entries.map(({ criterion, finding }) => `<li>${renderFindingEvidence(criterion, finding)}</li>`).join("")}</ul>` : '<p class="findings-first-empty">No critical or warning findings were identified across any criterion in this scan.</p>';
1303
+ return `<section class="findings-first-summary" aria-labelledby="findings-first-heading">
1304
+ <h2 id="findings-first-heading">Critical and warning findings</h2>
1305
+ ${body}
1306
+ </section>`;
1307
+ }
1157
1308
  function renderRelyingPartySummary(summary) {
1158
1309
  return `<section class="relying-party-summary" aria-labelledby="relying-party-heading">
1159
1310
  <h2 id="relying-party-heading">How to read this certificate</h2>
@@ -1167,10 +1318,12 @@ function renderRelyingPartySummary(summary) {
1167
1318
  }
1168
1319
  function renderGlossary(entries) {
1169
1320
  return `<section class="glossary" aria-labelledby="glossary-heading">
1170
- <h2 id="glossary-heading">Plain-language glossary</h2>
1171
- <dl>${entries.map(
1321
+ <details>
1322
+ <summary><h2 id="glossary-heading">Plain-language glossary</h2></summary>
1323
+ <dl>${entries.map(
1172
1324
  (entry) => `<div><dt id="glossary-${escapeAttribute(entry.key)}">${escapeHtml(entry.term)}</dt><dd>${escapeHtml(entry.definition)}</dd></div>`
1173
1325
  ).join("")}</dl>
1326
+ </details>
1174
1327
  </section>`;
1175
1328
  }
1176
1329
  function renderContentReadSummary(report) {
@@ -1245,12 +1398,12 @@ function renderCriterionColumn(title, criteria, gitHistoryUnavailable) {
1245
1398
  </section>`;
1246
1399
  }
1247
1400
  function renderNotApplicableGroup(criteria) {
1248
- return `<div class="na-group">
1249
- <h3 class="na-heading">Not applicable to this repository</h3>
1401
+ return `<details class="na-group">
1402
+ <summary><h3 class="na-heading">Not applicable to this repository</h3></summary>
1250
1403
  <ul class="na-list">
1251
1404
  ${criteria.map(renderNotApplicableItem).join("")}
1252
1405
  </ul>
1253
- </div>`;
1406
+ </details>`;
1254
1407
  }
1255
1408
  function renderNotApplicableItem(criterion) {
1256
1409
  const reason = criterion.notes ?? "Not applicable to this repository.";
@@ -1310,15 +1463,21 @@ function scoreBandForPresentation(score) {
1310
1463
  if (score >= 1.5) return "warning";
1311
1464
  return "critical";
1312
1465
  }
1466
+ function renderMetricWeightBadge(criterion, metric2) {
1467
+ const appliedSharePercent = formatCertificateMetricAppliedWeightPercent(criterion, metric2);
1468
+ return `<span class="metric-weight">weight ${appliedSharePercent}% of ${escapeHtml(criterion.id)}</span>`;
1469
+ }
1313
1470
  function renderMetric(criterion, metric2) {
1314
1471
  const entry = glossaryEntryForMetric(metric2);
1315
1472
  const tooltipId = `tooltip-${criterion.id}-${entry.key}`;
1316
1473
  const formatted = formatCertificateMetricValue(criterion, metric2);
1474
+ const weightBadge = renderMetricWeightBadge(criterion, metric2);
1317
1475
  const cappedEntry = formatted.includes("capped") ? CERTIFICATE_GLOSSARY.find((candidate) => candidate.key === "capped") : void 0;
1318
1476
  const label = `<span class="term-help" tabindex="0" aria-describedby="${escapeAttribute(tooltipId)}"><strong>${escapeHtml(formatCertificateMetricLabel(metric2))}</strong><span class="term-tooltip" id="${escapeAttribute(tooltipId)}" role="tooltip">${escapeHtml(entry.definition)}</span></span>`;
1319
- if (!cappedEntry) return `${label}<span class="metric-value">${escapeHtml(formatted)}</span>`;
1477
+ if (!cappedEntry)
1478
+ return `${label}<span class="metric-value">${escapeHtml(formatted)} ${weightBadge}</span>`;
1320
1479
  const cappedTooltipId = `${tooltipId}-capped`;
1321
- return `${label}<span class="term-help metric-value" tabindex="0" aria-describedby="${escapeAttribute(cappedTooltipId)}">${escapeHtml(formatted)}<span class="term-tooltip" id="${escapeAttribute(cappedTooltipId)}" role="tooltip">${escapeHtml(cappedEntry.definition)}</span></span>`;
1480
+ return `${label}<span class="term-help metric-value" tabindex="0" aria-describedby="${escapeAttribute(cappedTooltipId)}">${escapeHtml(formatted)} ${weightBadge}<span class="term-tooltip" id="${escapeAttribute(cappedTooltipId)}" role="tooltip">${escapeHtml(cappedEntry.definition)}</span></span>`;
1322
1481
  }
1323
1482
  function renderCategoryScore(score, coverage, category) {
1324
1483
  const categoryCoverage = coverage.byCategory.find((entry) => entry.category === category);
@@ -1466,6 +1625,9 @@ dd { margin: 0; color: var(--muted); overflow-wrap: anywhere; }
1466
1625
  .subscores { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 18px; color: var(--muted); font-family: var(--mono); font-size: 12px; }
1467
1626
  .coverage-note { margin-top: 8px; color: var(--faint); font-family: var(--mono); font-size: 11px; }
1468
1627
  .source-counts { margin: 8px 0 0; padding-left: 18px; font-family: var(--mono); font-size: 12px; color: var(--muted); }
1628
+ .findings-first-summary { margin-top: 20px; border: 1px solid var(--line-strong); border-radius: 8px; background: var(--surface); padding: 22px; }
1629
+ .findings-first-summary ul { margin: 0; padding-left: 18px; display: grid; gap: 10px; }
1630
+ .findings-first-empty { margin: 0; color: var(--muted); }
1469
1631
  .relying-party-summary, .glossary { margin-top: 28px; border: 1px solid var(--line-strong); border-radius: 8px; background: var(--surface); padding: 22px; }
1470
1632
  .relying-party-summary dl { display: grid; gap: 14px; margin: 0; }
1471
1633
  .relying-party-summary dl div { display: grid; grid-template-columns: 190px minmax(0, 1fr); gap: 18px; }
@@ -1473,6 +1635,14 @@ dd { margin: 0; color: var(--muted); overflow-wrap: anywhere; }
1473
1635
  .glossary dl { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 22px; margin: 0; }
1474
1636
  .glossary dl div { border-top: 1px solid var(--line); padding-top: 10px; }
1475
1637
  .glossary dd { margin-top: 4px; font-size: 13px; }
1638
+ /* Track A3: native <details>/<summary> disclosure \u2014 the summary is the clickable control, a
1639
+ heading nests inside it as its accessible label (still exposed as a heading), and the heading's
1640
+ own vertical margin is neutralized by making it inline, so the marker sits flush against it. */
1641
+ summary { cursor: pointer; }
1642
+ summary h2, summary h3 { display: inline; }
1643
+ summary::marker { color: var(--periwinkle); }
1644
+ summary:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--periwinkle); border-radius: 3px; }
1645
+ .glossary details[open] dl, .na-group[open] .na-list { margin-top: 16px; }
1476
1646
  .trust-grid, .evidence-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; margin-top: 28px; }
1477
1647
  .scan-limitations-section { margin-top: 28px; border-color: rgba(231, 191, 114, .44); }
1478
1648
  .scan-limitations-section .scan-warning { list-style: none; margin-left: -18px; }
@@ -1504,6 +1674,7 @@ h3 { font-size: 15px; line-height: 1.35; font-weight: 600; margin-bottom: 0; }
1504
1674
  .criterion-metrics li { display: flex; justify-content: space-between; gap: 12px; border-bottom: 1px solid rgba(238, 244, 247, .08); padding-bottom: 6px; }
1505
1675
  .criterion-metrics strong { color: var(--text); font-weight: 650; }
1506
1676
  .criterion-metrics span { color: var(--muted); }
1677
+ .criterion-metrics .metric-weight { font-size: 10px; color: var(--faint); }
1507
1678
  .criterion-metrics > li > .metric-value {
1508
1679
  min-width: 0; white-space: normal; overflow-wrap: anywhere; text-align: right;
1509
1680
  }
@@ -1667,6 +1838,7 @@ function createWitanAttestation(report, options) {
1667
1838
  tool: { name: "cejel", version: options.toolVersion },
1668
1839
  generatedAt: options.generatedAt,
1669
1840
  reportFormatVersion: WITAN_REPORT_FORMAT_VERSION,
1841
+ ...options.githubRunAttempt ? { githubRunAttempt: options.githubRunAttempt } : {},
1670
1842
  rubricVersion: report.rubricVersion,
1671
1843
  repository: {
1672
1844
  productSlug: report.productSlug,
@@ -1788,7 +1960,7 @@ function stripBom(text) {
1788
1960
 
1789
1961
  // src/witan/repo-signals.ts
1790
1962
  function usesV17DetectorClosure(rubricVersion) {
1791
- return rubricVersion === WITAN_RUBRIC_VERSION_V17 || rubricVersion === WITAN_RUBRIC_VERSION_V18 || rubricVersion === WITAN_RUBRIC_VERSION_V19 || rubricVersion === WITAN_RUBRIC_VERSION_V20 || rubricVersion === WITAN_RUBRIC_VERSION_V21 || rubricVersion === WITAN_RUBRIC_VERSION_V22;
1963
+ 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;
1792
1964
  }
1793
1965
  var LEGACY_INVENTORY_ABSENCE_CONTEXT = {
1794
1966
  rubricGeneration: "legacy",
@@ -1803,10 +1975,14 @@ var INVENTORY_SCAN_PATTERN_SETS = {
1803
1975
  id: "cejel.core-a1.concrete-test-files.v1",
1804
1976
  count: 16
1805
1977
  },
1806
- a1CoverageConfiguration: {
1978
+ a1CoverageConfigurationV1: {
1807
1979
  id: "cejel.core-a1.coverage-configuration.v1",
1808
1980
  count: 10
1809
1981
  },
1982
+ a1CoverageConfigurationV2: {
1983
+ id: "cejel.core-a1.coverage-configuration.v2",
1984
+ count: 14
1985
+ },
1810
1986
  a2CurrentSecret: {
1811
1987
  id: "cejel.core-a2.current-secret-shape.v1",
1812
1988
  count: 3
@@ -1855,25 +2031,127 @@ function inventoryAbsenceFinding(options) {
1855
2031
  } : {}
1856
2032
  };
1857
2033
  }
2034
+ function wholesaleCriterionAbstention(signal, reasons) {
2035
+ const { insufficientDataReason } = classifyAbstentionReason(reasons);
2036
+ return {
2037
+ criterionId: signal.criterionId,
2038
+ positiveEvidence: [],
2039
+ findings: [],
2040
+ metrics: [],
2041
+ insufficientData: true,
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."
2044
+ };
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
+ }
1858
2059
  function buildWitanInputFromRepo(options) {
1859
2060
  const tracked = trackContentReads(options.repoPath, () => buildWitanInputFromRepoUntracked(options));
1860
2061
  const affectedCriteria = tracked.affectedCriteria;
2062
+ const rubricVersion = options.rubricVersion ?? WITAN_RUBRIC_VERSION;
2063
+ if (rubricVersion !== WITAN_RUBRIC_VERSION_V23) {
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
+ });
2075
+ const scanLimitations2 = [...tracked.value.scanLimitations ?? []];
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) {
2082
+ scanLimitations2.push(
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.`
2084
+ );
2085
+ }
2086
+ return {
2087
+ ...tracked.value,
2088
+ signals: signals2,
2089
+ ...scanLimitations2.length > 0 ? { scanLimitations: scanLimitations2 } : {},
2090
+ contentReadSummary: tracked.summary
2091
+ };
2092
+ }
2093
+ const wholesaleAbstainedReadFailureCriteria = [];
2094
+ const wholesaleAbstainedCoverageLimitedCriteria = [];
2095
+ const partiallyAbstainedReadFailureSignals = [];
2096
+ const partiallyAbstainedCoverageLimitedSignals = [];
1861
2097
  const signals = (tracked.value.signals ?? []).map((signal) => {
1862
2098
  if (!affectedCriteria.has(signal.criterionId)) return signal;
2099
+ if (tracked.unattributedCriteria.has(signal.criterionId)) {
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);
2104
+ }
2105
+ const prefix = contentReadSignalKey(signal.criterionId, "");
2106
+ const affectedNames = [...tracked.affectedSignals].filter((key) => key.startsWith(prefix)).map((key) => key.slice(prefix.length));
2107
+ if (affectedNames.length === 0) {
2108
+ wholesaleAbstainedReadFailureCriteria.push(signal.criterionId);
2109
+ return wholesaleCriterionAbstention(signal);
2110
+ }
2111
+ const affectedNameSet = new Set(affectedNames);
2112
+ const metrics = (signal.metrics ?? []).filter((metric2) => !affectedNameSet.has(metric2.name));
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
+ }
2120
+ if (!anythingSurvives) {
2121
+ const { insufficientDataReason: insufficientDataReason2 } = classifyAbstentionReason(droppedReasons);
2122
+ (insufficientDataReason2 !== void 0 ? wholesaleAbstainedCoverageLimitedCriteria : wholesaleAbstainedReadFailureCriteria).push(signal.criterionId);
2123
+ return wholesaleCriterionAbstention(signal, droppedReasons);
2124
+ }
2125
+ const { insufficientDataReason } = classifyAbstentionReason(droppedReasons);
2126
+ const isReadFailure = insufficientDataReason === void 0;
2127
+ for (const name of affectedNames) {
2128
+ (isReadFailure ? partiallyAbstainedReadFailureSignals : partiallyAbstainedCoverageLimitedSignals).push(`${signal.criterionId}.${name}`);
2129
+ }
1863
2130
  return {
1864
- criterionId: signal.criterionId,
1865
- positiveEvidence: [],
1866
- findings: [],
1867
- metrics: [],
1868
- insufficientData: true,
1869
- 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."
2131
+ ...signal,
2132
+ metrics,
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.`
1870
2134
  };
1871
2135
  });
1872
- const affectedCount = affectedCriteria.size;
1873
2136
  const scanLimitations = [...tracked.value.scanLimitations ?? []];
1874
- if (affectedCount > 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) {
1875
2148
  scanLimitations.push(
1876
- `${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.`
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(", ")}.`
2150
+ );
2151
+ }
2152
+ if (partiallyAbstainedCoverageLimitedSignals.length > 0 && scanLimitations.length < 16) {
2153
+ scanLimitations.push(
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.`
1877
2155
  );
1878
2156
  }
1879
2157
  return {
@@ -1897,11 +2175,13 @@ function buildWitanInputFromRepoUntracked(options) {
1897
2175
  rubricGeneration: usesV17Detectors ? "current" : "legacy",
1898
2176
  inventoryProvenance: fileInventory.repoFileInventoryScope === "tracked-scan-eligible" ? "tracked-scan-eligible" : "directory-walk"
1899
2177
  };
1900
- const usesV18NativeRls = rubricVersion === WITAN_RUBRIC_VERSION_V18 || rubricVersion === WITAN_RUBRIC_VERSION_V19 || rubricVersion === WITAN_RUBRIC_VERSION_V20 || rubricVersion === WITAN_RUBRIC_VERSION_V21 || rubricVersion === WITAN_RUBRIC_VERSION_V22;
1901
- const usesV19CommitYear = rubricVersion === WITAN_RUBRIC_VERSION_V19 || rubricVersion === WITAN_RUBRIC_VERSION_V20 || rubricVersion === WITAN_RUBRIC_VERSION_V21 || rubricVersion === WITAN_RUBRIC_VERSION_V22;
1902
- const usesV20A3ExplicitGaps = rubricVersion === WITAN_RUBRIC_VERSION_V20 || rubricVersion === WITAN_RUBRIC_VERSION_V21 || rubricVersion === WITAN_RUBRIC_VERSION_V22;
1903
- const usesV21ExecutedEscalations = rubricVersion === WITAN_RUBRIC_VERSION_V21 || rubricVersion === WITAN_RUBRIC_VERSION_V22;
1904
- const usesV22PackageStartEntrypoint = rubricVersion === WITAN_RUBRIC_VERSION_V22;
2178
+ const usesV18NativeRls = rubricVersion === WITAN_RUBRIC_VERSION_V18 || rubricVersion === WITAN_RUBRIC_VERSION_V19 || rubricVersion === WITAN_RUBRIC_VERSION_V20 || rubricVersion === WITAN_RUBRIC_VERSION_V21 || rubricVersion === WITAN_RUBRIC_VERSION_V22 || rubricVersion === WITAN_RUBRIC_VERSION_V23;
2179
+ const usesV19CommitYear = 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;
2180
+ const usesV20A3ExplicitGaps = rubricVersion === WITAN_RUBRIC_VERSION_V20 || rubricVersion === WITAN_RUBRIC_VERSION_V21 || rubricVersion === WITAN_RUBRIC_VERSION_V22 || rubricVersion === WITAN_RUBRIC_VERSION_V23;
2181
+ const usesV21ExecutedEscalations = rubricVersion === WITAN_RUBRIC_VERSION_V21 || rubricVersion === WITAN_RUBRIC_VERSION_V22 || rubricVersion === WITAN_RUBRIC_VERSION_V23;
2182
+ const usesV22PackageStartEntrypoint = rubricVersion === WITAN_RUBRIC_VERSION_V22 || rubricVersion === WITAN_RUBRIC_VERSION_V23;
2183
+ const usesV23CommandCoverage = rubricVersion === WITAN_RUBRIC_VERSION_V23;
2184
+ const usesV23PemPrivateKeyGrammar = rubricVersion === WITAN_RUBRIC_VERSION_V23;
1905
2185
  const structuralArchetype = classifyRepoArchetype(inventoryFiles, rubricVersion);
1906
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(
1907
2187
  options.repoPath,
@@ -1937,6 +2217,8 @@ function buildWitanInputFromRepoUntracked(options) {
1937
2217
  usesV20A3ExplicitGaps,
1938
2218
  usesV21ExecutedEscalations,
1939
2219
  usesV22PackageStartEntrypoint,
2220
+ usesV23CommandCoverage,
2221
+ usesV23PemPrivateKeyGrammar,
1940
2222
  reviewableSourceProof,
1941
2223
  inventoryAbsenceContext,
1942
2224
  scanLimitations
@@ -2548,7 +2830,7 @@ function buildNotApplicableSignal(criterionId, reason) {
2548
2830
  notApplicable: true
2549
2831
  };
2550
2832
  }
2551
- function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV18NativeRls, useV19CommitYear, useV20A3ExplicitGaps, useV21ExecutedEscalations, useV22PackageStartEntrypoint, reviewableSourceProof, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT, scanLimitations = /* @__PURE__ */ new Set()) {
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()) {
2552
2834
  const signals = [];
2553
2835
  const mono = resolveMonorepoContext(repoPath, scanLimitations);
2554
2836
  const collectCriterion = (criterionId, collect) => withContentReadCriterion(criterionId, collect);
@@ -2560,6 +2842,7 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
2560
2842
  useV27Detectors,
2561
2843
  useV33Detectors,
2562
2844
  useV36Detectors,
2845
+ useV23CommandCoverage,
2563
2846
  reviewableSourceProof,
2564
2847
  inventoryAbsenceContext
2565
2848
  )
@@ -2574,6 +2857,7 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
2574
2857
  useV36Detectors,
2575
2858
  useV39Detectors,
2576
2859
  useV47Detectors,
2860
+ useV23PemPrivateKeyGrammar,
2577
2861
  useV18NativeRls,
2578
2862
  inventoryAbsenceContext,
2579
2863
  scanLimitations
@@ -2659,7 +2943,7 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
2659
2943
  }
2660
2944
  return signals;
2661
2945
  }
2662
- function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, reviewableSourceProof, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT) {
2946
+ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV23CommandCoverage, reviewableSourceProof, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT) {
2663
2947
  const evidence = [];
2664
2948
  const findings = [];
2665
2949
  const allTestFiles = findConcreteTestFiles(
@@ -2670,17 +2954,33 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
2670
2954
  const testFiles = allTestFiles.slice(0, 8);
2671
2955
  const sourceFiles = repoFiles.filter(isSourceFile);
2672
2956
  const runnerFiles = repoFiles.filter(
2673
- (file) => (!useV27Detectors || isAuthoredProductionPath(file)) && isTestRunnerConfig(repoPath, file)
2957
+ (file) => (!useV27Detectors || isAuthoredProductionPath(file)) && (isTestRunnerConfig(repoPath, file) || useV23CommandCoverage && isRecipeTestEntry(repoPath, file))
2674
2958
  ).slice(0, 6);
2675
- const configuredRunnerFiles = useV33Detectors ? findConfiguredTestRunnerFiles(repoPath, repoFiles) : runnerFiles;
2676
- const allCoverageFiles = findCoverageConfigFiles(repoPath, repoFiles, useV27Detectors);
2677
- const coverageFiles = allCoverageFiles.slice(0, 4);
2959
+ const baseConfiguredRunnerFiles = useV33Detectors ? findConfiguredTestRunnerFiles(repoPath, repoFiles) : runnerFiles;
2960
+ const configuredRunnerFiles = useV23CommandCoverage ? [
2961
+ .../* @__PURE__ */ new Set([
2962
+ ...baseConfiguredRunnerFiles,
2963
+ ...repoFiles.filter((file) => isRecipeTestEntry(repoPath, file))
2964
+ ])
2965
+ ].sort() : baseConfiguredRunnerFiles;
2678
2966
  const packageJson = findRootPackageJson(repoFiles);
2679
2967
  const packageScripts = packageJson ? readPackageScripts(join(repoPath, packageJson)) : /* @__PURE__ */ new Map();
2968
+ const { coverageAnalysis, coveragePercent } = withContentReadSignal("A1", "coverage_percent", () => {
2969
+ const analysis = useV23CommandCoverage ? analyzeCoverageConfiguration(repoPath, repoFiles, useV27Detectors) : {
2970
+ coverageFiles: findCoverageConfigFiles(repoPath, repoFiles, useV27Detectors),
2971
+ unresolvedFiles: [],
2972
+ hasCoverageCommand: packageScripts.has("coverage")
2973
+ };
2974
+ return {
2975
+ coverageAnalysis: analysis,
2976
+ coveragePercent: readCoveragePercent(repoPath, analysis.coverageFiles.slice(0, 4))
2977
+ };
2978
+ });
2979
+ const allCoverageFiles = coverageAnalysis.coverageFiles;
2980
+ const coverageFiles = allCoverageFiles.slice(0, 4);
2680
2981
  const packageJsonFiles = useV27Detectors ? repoFiles.filter(
2681
2982
  (file) => /(^|\/)package\.json$/.test(file) && isAuthoredProductionPath(file)
2682
2983
  ) : packageJson ? [packageJson] : [];
2683
- const coveragePercent = readCoveragePercent(repoPath, coverageFiles);
2684
2984
  const ciWorkflows = repoFiles.filter(
2685
2985
  (file) => (!useV27Detectors || isAuthoredProductionPath(file)) && isCiWorkflow(file)
2686
2986
  );
@@ -2695,7 +2995,7 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
2695
2995
  );
2696
2996
  const verificationScriptCount = [
2697
2997
  packageScripts.has("test") || Boolean(ciTestWorkflow),
2698
- packageScripts.has("coverage"),
2998
+ coverageAnalysis.hasCoverageCommand,
2699
2999
  packageScripts.has("lint") || ciHasLintCommand,
2700
3000
  packageScripts.has("typecheck") || ciHasTypecheckCommand
2701
3001
  ].filter(Boolean).length;
@@ -2715,9 +3015,11 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
2715
3015
  if (packageJson && [...packageScripts.values()].some((script) => TEST_RUNNER_PATTERN.test(script))) {
2716
3016
  evidence.push(evidenceForRelative(repoPath, packageJson, "test_run", "Configured test runner"));
2717
3017
  }
2718
- for (const file of coverageFiles) {
2719
- evidence.push(evidenceForRelative(repoPath, file, "coverage", "Coverage configuration"));
2720
- }
3018
+ withContentReadSignal("A1", "coverage_percent", () => {
3019
+ for (const file of coverageFiles) {
3020
+ evidence.push(evidenceForRelative(repoPath, file, "coverage", "Coverage configuration"));
3021
+ }
3022
+ });
2721
3023
  if (ciTestWorkflow) {
2722
3024
  evidence.push(
2723
3025
  evidenceForRelative(repoPath, ciTestWorkflow, "test_run", "CI workflow runs the test suite")
@@ -2787,7 +3089,7 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
2787
3089
  })
2788
3090
  );
2789
3091
  }
2790
- } else if (coverageFiles.length === 0 && (!useV33Detectors || configuredRunnerFiles.length > 0)) {
3092
+ } else if (coverageFiles.length === 0 && coverageAnalysis.unresolvedFiles.length === 0 && (!useV33Detectors || configuredRunnerFiles.length > 0)) {
2791
3093
  if (isLeanTestToolchain) {
2792
3094
  const leanEvidenceFile = packageJson ?? testFiles[0];
2793
3095
  if (leanEvidenceFile) {
@@ -2814,16 +3116,16 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
2814
3116
  evidence: firstEvidence,
2815
3117
  context: inventoryAbsenceContext,
2816
3118
  inventoryCount: repoFiles.length,
2817
- patternSet: INVENTORY_SCAN_PATTERN_SETS.a1CoverageConfiguration,
3119
+ patternSet: useV23CommandCoverage ? INVENTORY_SCAN_PATTERN_SETS.a1CoverageConfigurationV2 : INVENTORY_SCAN_PATTERN_SETS.a1CoverageConfigurationV1,
2818
3120
  matchCount: 0
2819
3121
  })
2820
3122
  );
2821
3123
  }
2822
3124
  }
2823
- const nonHollowShare = measureNonHollowTestShare(
2824
- repoPath,
2825
- allTestFiles,
2826
- reviewableSourceProof !== void 0
3125
+ const nonHollowShare = withContentReadSignal(
3126
+ "A1",
3127
+ "non_hollow_test_share",
3128
+ () => measureNonHollowTestShare(repoPath, allTestFiles, reviewableSourceProof !== void 0)
2827
3129
  );
2828
3130
  return {
2829
3131
  criterionId: "A1",
@@ -2864,7 +3166,7 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
2864
3166
  label: "test command",
2865
3167
  count: packageScripts.has("test") || Boolean(ciTestWorkflow) ? 1 : 0
2866
3168
  },
2867
- { label: "coverage command", count: packageScripts.has("coverage") ? 1 : 0 },
3169
+ { label: "coverage command", count: coverageAnalysis.hasCoverageCommand ? 1 : 0 },
2868
3170
  {
2869
3171
  label: "lint command",
2870
3172
  count: packageScripts.has("lint") || ciHasLintCommand ? 1 : 0
@@ -2887,7 +3189,7 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
2887
3189
  "Penalizes skipped or placeholder-only test files; test-directory support scaffolding (helpers/fixtures with no test in them) is excluded from the denominator."
2888
3190
  )
2889
3191
  ],
2890
- notes: "A1 is detected from real test files, test runner configuration, and optional coverage configuration."
3192
+ 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."
2891
3193
  };
2892
3194
  }
2893
3195
  function buildA1AuthenticatedAbsenceSignal(repoPath, proof, inventoryCount, inventoryAbsenceContext) {
@@ -2971,7 +3273,7 @@ function buildA1AuthenticatedAbsenceSignal(repoPath, proof, inventoryCount, inve
2971
3273
  notes: `V17 authenticated absence: ${proof.eligibleSourceFileCount} criterion-ratable of ${proof.sourceShapedFileCount} source-shaped files; sampled ${proof.sampledFileCount}; ${proof.readableSampledFileCount} representative-text files.`
2972
3274
  };
2973
3275
  }
2974
- 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) {
2975
3277
  const evidence = [];
2976
3278
  const findings = [];
2977
3279
  const gitignore = repoFiles.find((file) => basename(file) === ".gitignore");
@@ -2979,40 +3281,51 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
2979
3281
  const migrationFiles = repoFiles.filter(
2980
3282
  (file) => /(^|\/)(migrations?|drizzle|prisma)\//.test(file)
2981
3283
  );
2982
- let committedSecret;
2983
- for (const path of repoFiles) {
2984
- const authoredProductionPath = useV47Detectors ? isV47AuthoredProductionPath(path) : useV39Detectors ? isV39AuthoredProductionPath(path) : isAuthoredProductionPath(path);
2985
- if (isIgnoredScanFile(path, useV47Detectors) || useV33Detectors && !authoredProductionPath) {
2986
- continue;
2987
- }
2988
- const match = findCommittedSecretInFile(
2989
- repoPath,
2990
- path,
2991
- useV36Detectors,
2992
- useV39Detectors,
2993
- useV47Detectors
2994
- );
2995
- if (!match) continue;
2996
- committedSecret = { path, match };
2997
- break;
2998
- }
2999
- const currentSecretFingerprintsByPath = /* @__PURE__ */ new Map();
3000
- if (useV27Detectors) {
3001
- for (const path of repoFiles.filter((candidate) => isCredentialHistoryPath(candidate))) {
3002
- currentSecretFingerprintsByPath.set(
3003
- path,
3004
- new Set(
3005
- findSecretFingerprintsInFile(
3006
- repoPath,
3284
+ const { committedSecret, currentSecretFingerprintsByPath } = withContentReadSignal(
3285
+ "A2",
3286
+ "secret_cleanliness",
3287
+ () => {
3288
+ let match_;
3289
+ for (const path of repoFiles) {
3290
+ const authoredProductionPath = useV47Detectors ? isV47AuthoredProductionPath(path) : useV39Detectors ? isV39AuthoredProductionPath(path) : isAuthoredProductionPath(path);
3291
+ if (isIgnoredScanFile(path, useV47Detectors) || useV33Detectors && !authoredProductionPath) {
3292
+ continue;
3293
+ }
3294
+ const found = findCommittedSecretInFile(
3295
+ repoPath,
3296
+ path,
3297
+ useV36Detectors,
3298
+ useV39Detectors,
3299
+ useV47Detectors,
3300
+ useV23PemPrivateKeyGrammar
3301
+ );
3302
+ if (!found) continue;
3303
+ match_ = { path, match: found };
3304
+ break;
3305
+ }
3306
+ const fingerprintsByPath = /* @__PURE__ */ new Map();
3307
+ if (useV27Detectors) {
3308
+ for (const path of repoFiles.filter(
3309
+ (candidate) => isCredentialHistoryPath(candidate, useV36Detectors, useV23PemPrivateKeyGrammar)
3310
+ )) {
3311
+ fingerprintsByPath.set(
3007
3312
  path,
3008
- useV36Detectors,
3009
- useV39Detectors,
3010
- useV47Detectors
3011
- )
3012
- )
3013
- );
3313
+ new Set(
3314
+ findSecretFingerprintsInFile(
3315
+ repoPath,
3316
+ path,
3317
+ useV36Detectors,
3318
+ useV39Detectors,
3319
+ useV47Detectors,
3320
+ useV23PemPrivateKeyGrammar
3321
+ )
3322
+ )
3323
+ );
3324
+ }
3325
+ }
3326
+ return { committedSecret: match_, currentSecretFingerprintsByPath: fingerprintsByPath };
3014
3327
  }
3015
- }
3328
+ );
3016
3329
  const historySecretScan = committedSecret && !useV27Detectors ? null : collectHistorySecretEvidence(
3017
3330
  repoPath,
3018
3331
  useV27Detectors,
@@ -3021,13 +3334,18 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
3021
3334
  useV33Detectors,
3022
3335
  useV36Detectors,
3023
3336
  useV39Detectors,
3024
- useV47Detectors
3337
+ useV47Detectors,
3338
+ useV23PemPrivateKeyGrammar
3025
3339
  );
3026
3340
  const hasConfirmedSecretFinding = committedSecret != null || historySecretScan?.evidence != null;
3027
- const cryptoHygiene = collectCryptoHygieneEvidence(
3028
- repoPath,
3029
- repoFiles.filter(isImplementationFile).slice(0, 60),
3030
- useV39Detectors
3341
+ const cryptoHygiene = withContentReadSignal(
3342
+ "A2",
3343
+ "crypto_comparison_hygiene",
3344
+ () => collectCryptoHygieneEvidence(
3345
+ repoPath,
3346
+ repoFiles.filter(isImplementationFile).slice(0, 60),
3347
+ useV39Detectors
3348
+ )
3031
3349
  );
3032
3350
  const implFiles = repoFiles.filter(isImplementationFile);
3033
3351
  const gitignoreHasEnvRule = gitignore != null && fileContains(repoPath, gitignore, /^\.env(\*|\b)|\.env\./m);
@@ -3067,57 +3385,93 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
3067
3385
  } else if (dbLayerImportFile != null) {
3068
3386
  evidence.push(evidenceForRelative(repoPath, dbLayerImportFile, "artifact", "DB client import"));
3069
3387
  }
3070
- const v47RlsPolicyFiles = useV47Detectors ? findRlsPolicyFiles(repoPath, repoFiles) : [];
3071
- const nativeRlsInventory = useV18NativeRls ? deriveRlsPolicyScopeInventory(repoPath, repoFiles) : null;
3072
- const tenantWithoutRlsPremiseFiles = useV18NativeRls ? findTenantStoragePremiseFiles(repoPath, repoFiles) : [];
3073
- const v47TenantStorageFiles = useV47Detectors && !useV18NativeRls ? findTenantStoragePremiseFiles(repoPath, repoFiles) : [];
3074
- const rlsMigration = useV47Detectors ? v47RlsPolicyFiles[0] : migrationFiles.find((file) => fileContains(repoPath, file, RLS_PATTERN));
3075
3388
  const tenantScopePattern = useV39Detectors ? TENANT_SCOPE_PATTERN_V11 : useV36Detectors ? TENANT_SCOPE_PATTERN_V10 : TENANT_SCOPE_PATTERN;
3076
- const tenantScopedFile = useV18NativeRls ? nativeRlsInventory?.storageFiles[0] : useV47Detectors ? v47TenantStorageFiles[0] : migrationFiles.find((file) => fileContains(repoPath, file, tenantScopePattern));
3077
- const rlsPolicyCount = useV18NativeRls ? nativeRlsInventory?.policyCount ?? 0 : useV47Detectors ? v47RlsPolicyFiles.length : countPatternMatches(
3078
- repoPath,
3079
- migrationFiles,
3080
- /create policy|enable row level security|force row level security/gi
3389
+ const {
3390
+ rlsMigration,
3391
+ tenantScopedFile,
3392
+ rlsPolicyCount,
3393
+ tenantScopedMigrationFileCount,
3394
+ nativeRlsInventory,
3395
+ tenantWithoutRlsPremiseFiles
3396
+ } = withContentReadSignals("A2", ["rls_policy_count", "tenant_scope_ratio"], () => {
3397
+ const v47RlsPolicyFiles = useV47Detectors ? findRlsPolicyFiles(repoPath, repoFiles) : [];
3398
+ const rlsInventory = useV18NativeRls ? deriveRlsPolicyScopeInventory(repoPath, repoFiles) : null;
3399
+ const tenantWithoutRlsFiles = useV18NativeRls ? findTenantStoragePremiseFiles(repoPath, repoFiles) : [];
3400
+ const v47TenantStorageFiles = useV47Detectors && !useV18NativeRls ? findTenantStoragePremiseFiles(repoPath, repoFiles) : [];
3401
+ return {
3402
+ rlsMigration: useV47Detectors ? v47RlsPolicyFiles[0] : migrationFiles.find((file) => fileContains(repoPath, file, RLS_PATTERN)),
3403
+ tenantScopedFile: useV18NativeRls ? rlsInventory?.storageFiles[0] : useV47Detectors ? v47TenantStorageFiles[0] : migrationFiles.find((file) => fileContains(repoPath, file, tenantScopePattern)),
3404
+ rlsPolicyCount: useV18NativeRls ? rlsInventory?.policyCount ?? 0 : useV47Detectors ? v47RlsPolicyFiles.length : countPatternMatches(
3405
+ repoPath,
3406
+ migrationFiles,
3407
+ /create policy|enable row level security|force row level security/gi
3408
+ ),
3409
+ tenantScopedMigrationFileCount: useV18NativeRls ? rlsInventory?.storageFiles.length ?? 0 : useV47Detectors ? v47TenantStorageFiles.length : countFilesContaining(repoPath, migrationFiles, tenantScopePattern),
3410
+ nativeRlsInventory: rlsInventory,
3411
+ tenantWithoutRlsPremiseFiles: tenantWithoutRlsFiles
3412
+ };
3413
+ });
3414
+ const { gitignoreEnvFile, hasEnvReadsInImplementation, envHandlingDepth } = withContentReadSignal(
3415
+ "A2",
3416
+ "env_handling_depth",
3417
+ () => {
3418
+ const envFile = gitignore && fileContains(repoPath, gitignore, /^\.env(\*|\b)|\.env\./m) ? gitignore : null;
3419
+ const hasEnvReads = countFilesContaining(repoPath, repoFiles.filter(isImplementationFile), ENV_READ_PATTERN) > 0;
3420
+ const depth = (envExamples.length > 0 ? 1 : 0) + (envFile ? 1 : 0) + (hasEnvReads ? 1 : 0);
3421
+ return { gitignoreEnvFile: envFile, hasEnvReadsInImplementation: hasEnvReads, envHandlingDepth: depth };
3422
+ }
3081
3423
  );
3082
- const tenantScopedMigrationFileCount = useV18NativeRls ? nativeRlsInventory?.storageFiles.length ?? 0 : useV47Detectors ? v47TenantStorageFiles.length : countFilesContaining(repoPath, migrationFiles, tenantScopePattern);
3083
- const gitignoreEnvFile = gitignore && fileContains(repoPath, gitignore, /^\.env(\*|\b)|\.env\./m) ? gitignore : null;
3084
- const envHandlingDepth = (envExamples.length > 0 ? 1 : 0) + (gitignoreEnvFile ? 1 : 0) + (countFilesContaining(repoPath, repoFiles.filter(isImplementationFile), ENV_READ_PATTERN) > 0 ? 1 : 0);
3085
3424
  const isMultiTenant = useV18NativeRls ? (nativeRlsInventory?.policyCount ?? 0) > 0 && (nativeRlsInventory?.scopeIdentifiers.length ?? 0) > 0 : tenantScopedMigrationFileCount > 0;
3086
3425
  if (gitignoreEnvFile) {
3087
- evidence.push(
3088
- evidenceForRelative(repoPath, gitignoreEnvFile, "secret_scan", ".env files are gitignored")
3426
+ withContentReadSignal(
3427
+ "A2",
3428
+ "env_handling_depth",
3429
+ () => evidence.push(
3430
+ evidenceForRelative(repoPath, gitignoreEnvFile, "secret_scan", ".env files are gitignored")
3431
+ )
3089
3432
  );
3090
3433
  }
3091
- for (const file of envExamples.slice(0, 3)) {
3092
- evidence.push(evidenceForRelative(repoPath, file, "secret_scan", "Environment template"));
3093
- }
3434
+ withContentReadSignal("A2", "env_handling_depth", () => {
3435
+ for (const file of envExamples.slice(0, 3)) {
3436
+ evidence.push(evidenceForRelative(repoPath, file, "secret_scan", "Environment template"));
3437
+ }
3438
+ });
3094
3439
  if (rlsMigration) {
3095
- evidence.push(
3096
- evidenceForRelative(repoPath, rlsMigration, "artifact", "RLS or tenant migration")
3440
+ withContentReadSignals(
3441
+ "A2",
3442
+ ["rls_policy_count", "tenant_scope_ratio"],
3443
+ () => evidence.push(
3444
+ evidenceForRelative(repoPath, rlsMigration, "artifact", "RLS or tenant migration")
3445
+ )
3097
3446
  );
3098
3447
  }
3099
3448
  if (tenantScopedFile) {
3100
- evidence.push(
3101
- evidenceForRelative(
3102
- repoPath,
3103
- tenantScopedFile,
3104
- "artifact",
3105
- useV18NativeRls ? `Repository-native RLS scope (${nativeRlsInventory?.scopeIdentifiers.join(", ")})` : "Tenant scoping signal"
3449
+ withContentReadSignals(
3450
+ "A2",
3451
+ ["rls_policy_count", "tenant_scope_ratio"],
3452
+ () => evidence.push(
3453
+ evidenceForRelative(
3454
+ repoPath,
3455
+ tenantScopedFile,
3456
+ "artifact",
3457
+ useV18NativeRls ? `Repository-native RLS scope (${nativeRlsInventory?.scopeIdentifiers.join(", ")})` : "Tenant scoping signal"
3458
+ )
3106
3459
  )
3107
3460
  );
3108
3461
  }
3109
3462
  if (committedSecret) {
3110
3463
  const isTestPath = isTestOrFixturePath(committedSecret.path);
3111
3464
  const isDefaultAdmin = committedSecret.match.kind === "default_admin";
3465
+ const isPemPrivateKey = committedSecret.match.kind === "pem_private_key";
3112
3466
  findings.push({
3113
3467
  severity: isTestPath ? "info" : "critical",
3114
- 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.",
3115
3469
  evidence: evidenceForRelativeAtLine(
3116
3470
  repoPath,
3117
3471
  committedSecret.path,
3118
3472
  "secret_scan",
3119
3473
  secretEvidenceLabel(
3120
- 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",
3121
3475
  committedSecret.match
3122
3476
  ),
3123
3477
  committedSecret.match.line
@@ -3148,9 +3502,10 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
3148
3502
  if (historySecretScan?.evidence) {
3149
3503
  const historyPath = historySecretScan.evidence.path ?? "";
3150
3504
  const isTestPath = isTestOrFixturePath(historyPath);
3505
+ const isPemPrivateKey = historySecretScan.secretKind === "pem_private_key";
3151
3506
  findings.push({
3152
3507
  severity: isTestPath ? "info" : "critical",
3153
- 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.",
3154
3509
  evidence: historySecretScan.evidence
3155
3510
  });
3156
3511
  } else if (historySecretScan?.truncated) {
@@ -3226,11 +3581,7 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
3226
3581
  { label: ".gitignore environment-file rule", count: gitignoreEnvFile ? 1 : 0 },
3227
3582
  {
3228
3583
  label: "environment reads in implementation code",
3229
- count: countFilesContaining(
3230
- repoPath,
3231
- repoFiles.filter(isImplementationFile),
3232
- ENV_READ_PATTERN
3233
- ) > 0 ? 1 : 0
3584
+ count: hasEnvReadsInImplementation ? 1 : 0
3234
3585
  }
3235
3586
  ]
3236
3587
  }
@@ -3409,37 +3760,51 @@ function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, us
3409
3760
  const envTemplate = repoFiles.find(
3410
3761
  (file) => (!useV27Detectors || isAuthoredProductionPath(file)) && isEnvTemplatePath(file)
3411
3762
  );
3412
- const healthChecks = repoFiles.filter(
3413
- (file) => (!useV27Detectors || isAuthoredProductionPath(file)) && isHealthCheckSignalFile(repoPath, file)
3763
+ const healthChecks = withContentReadSignal(
3764
+ "A3",
3765
+ "prod_readiness_primitives",
3766
+ () => repoFiles.filter(
3767
+ (file) => (!useV27Detectors || isAuthoredProductionPath(file)) && isHealthCheckSignalFile(repoPath, file)
3768
+ )
3414
3769
  );
3415
3770
  const healthCheck = healthChecks[0];
3416
- const hasV20HealthOrReadinessRoute = useV20ExplicitGaps && repoFiles.some(
3417
- (file) => isAuthoredProductionPath(file) && isImplementationFile(file) && fileContains(repoPath, file, V20_HEALTH_OR_READINESS_ROUTE_PATTERN)
3418
- );
3419
- const hasV22PackageStartHealthOrReadinessRoute = v22PackageStartHttpEntrypoint !== null && fileContains(
3420
- repoPath,
3421
- v22PackageStartHttpEntrypoint,
3422
- V20_HEALTH_OR_READINESS_ROUTE_PATTERN
3423
- );
3771
+ const { hasV20HealthOrReadinessRoute, hasV22PackageStartHealthOrReadinessRoute } = withContentReadSignal("A3", "health_readiness_route", () => ({
3772
+ hasV20HealthOrReadinessRoute: useV20ExplicitGaps && repoFiles.some(
3773
+ (file) => isAuthoredProductionPath(file) && isImplementationFile(file) && fileContains(repoPath, file, V20_HEALTH_OR_READINESS_ROUTE_PATTERN)
3774
+ ),
3775
+ hasV22PackageStartHealthOrReadinessRoute: v22PackageStartHttpEntrypoint !== null && fileContains(
3776
+ repoPath,
3777
+ v22PackageStartHttpEntrypoint,
3778
+ V20_HEALTH_OR_READINESS_ROUTE_PATTERN
3779
+ )
3780
+ }));
3424
3781
  const serverEntrypoint = v22PackageStartHttpEntrypoint ?? findServerEntrypointFile(repoPath, repoFiles, useV27Detectors) ?? v20DirectHttpEntrypoint ?? null;
3425
3782
  const runtimeContainer = useV27Detectors ? findRuntimeContainerEntrypointFile(repoPath, repoFiles, useV20ExplicitGaps) : null;
3426
3783
  const errorBoundaries = repoFiles.filter(
3427
3784
  (file) => (!useV27Detectors || isAuthoredProductionPath(file)) && /error-boundary|error\.(tsx|jsx|ts|js)$/.test(file)
3428
3785
  );
3429
3786
  const errorBoundary = errorBoundaries[0];
3430
- const observabilityCount = countFilesContaining(
3431
- repoPath,
3432
- repoFiles.filter(
3433
- (file) => (!useV27Detectors || isAuthoredProductionPath(file)) && isImplementationFile(file)
3434
- ),
3435
- /sentry|otel|opentelemetry|datadog|prometheus|metrics|logger|logtail/i
3787
+ const observabilityCount = withContentReadSignal(
3788
+ "A3",
3789
+ "observability_depth",
3790
+ () => countFilesContaining(
3791
+ repoPath,
3792
+ repoFiles.filter(
3793
+ (file) => (!useV27Detectors || isAuthoredProductionPath(file)) && isImplementationFile(file)
3794
+ ),
3795
+ /sentry|otel|opentelemetry|datadog|prometheus|metrics|logger|logtail/i
3796
+ )
3436
3797
  );
3437
- const rollbackSafetyCount = countFilesContaining(
3438
- repoPath,
3439
- repoFiles.filter(
3440
- (file) => (!useV27Detectors || isAuthoredProductionPath(file)) && /(^|\/)(docs|migrations?|drizzle|prisma|scripts)\//.test(file)
3441
- ),
3442
- /rollback|roll back|migration safety|down migration|reversible|undo migration/i
3798
+ const rollbackSafetyCount = withContentReadSignal(
3799
+ "A3",
3800
+ "rollback_safety_depth",
3801
+ () => countFilesContaining(
3802
+ repoPath,
3803
+ repoFiles.filter(
3804
+ (file) => (!useV27Detectors || isAuthoredProductionPath(file)) && /(^|\/)(docs|migrations?|drizzle|prisma|scripts)\//.test(file)
3805
+ ),
3806
+ /rollback|roll back|migration safety|down migration|reversible|undo migration/i
3807
+ )
3443
3808
  );
3444
3809
  if (packageJson && (scripts.has("build") || scripts.has("typecheck"))) {
3445
3810
  evidence.push(
@@ -3467,10 +3832,15 @@ function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, us
3467
3832
  );
3468
3833
  if (envTemplate)
3469
3834
  evidence.push(evidenceForRelative(repoPath, envTemplate, "prod_check", "Environment template"));
3470
- if (healthCheck)
3471
- evidence.push(
3472
- evidenceForRelative(repoPath, healthCheck, "prod_check", "Health/readiness signal")
3835
+ if (healthCheck) {
3836
+ withContentReadSignal(
3837
+ "A3",
3838
+ "prod_readiness_primitives",
3839
+ () => evidence.push(
3840
+ evidenceForRelative(repoPath, healthCheck, "prod_check", "Health/readiness signal")
3841
+ )
3473
3842
  );
3843
+ }
3474
3844
  if (errorBoundary)
3475
3845
  evidence.push(evidenceForRelative(repoPath, errorBoundary, "prod_check", "Error boundary"));
3476
3846
  if (evidence.length === 0) {
@@ -3509,7 +3879,12 @@ function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, us
3509
3879
  )
3510
3880
  });
3511
3881
  } else if (runtimeContainer) {
3512
- if (!fileContains(repoPath, runtimeContainer, /^\s*HEALTHCHECK\s+(?!NONE\b)/im)) {
3882
+ const hasActiveHealthcheck = withContentReadSignal(
3883
+ "A3",
3884
+ "dockerfile_healthcheck",
3885
+ () => fileContains(repoPath, runtimeContainer, /^\s*HEALTHCHECK\s+(?!NONE\b)/im)
3886
+ );
3887
+ if (!hasActiveHealthcheck) {
3513
3888
  findings.push({
3514
3889
  severity: "info",
3515
3890
  summary: "A runtime Dockerfile declares no active HEALTHCHECK instruction.",
@@ -3521,10 +3896,14 @@ function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, us
3521
3896
  )
3522
3897
  });
3523
3898
  }
3524
- } else if (serverEntrypoint && fileContains(
3525
- repoPath,
3526
- serverEntrypoint,
3527
- /\b(?:request|req)\.(?:url|path|pathname|method)\b/i
3899
+ } else if (serverEntrypoint && withContentReadSignal(
3900
+ "A3",
3901
+ "health_readiness_route",
3902
+ () => fileContains(
3903
+ repoPath,
3904
+ serverEntrypoint,
3905
+ /\b(?:request|req)\.(?:url|path|pathname|method)\b/i
3906
+ )
3528
3907
  ) && healthChecks.length === 0 && !hasV20HealthOrReadinessRoute && !hasV22PackageStartHealthOrReadinessRoute) {
3529
3908
  findings.push({
3530
3909
  severity: "info",
@@ -3630,22 +4009,41 @@ function collectA4DependencyEvidence(repoPath, repoFiles, mono, useV27Detectors,
3630
4009
  const findings = [];
3631
4010
  const manifests = repoFiles.filter(isDependencyManifest);
3632
4011
  const lockfiles = repoFiles.filter(isLockfile);
3633
- const manifest = pickPrimaryDependencyManifest(repoPath, manifests);
3634
4012
  const lockfile = lockfiles[0];
3635
4013
  const rootLockfile = mono ? mono.sharedFiles.find(isLockfile) : void 0;
3636
4014
  const rootUpdateConfig = mono ? mono.sharedFiles.find(isDependencyUpdateConfig) : void 0;
3637
4015
  const hasLockfile = Boolean(lockfile) || Boolean(rootLockfile);
3638
4016
  const updateConfig = repoFiles.find(isDependencyUpdateConfig);
3639
4017
  const hasUpdateConfig = Boolean(updateConfig) || Boolean(rootUpdateConfig);
3640
- const auditConfig = manifests.some((file) => packageJsonHasAuditScript(repoPath, file));
3641
- const dependencySpecs = readDependencySpecs(repoPath, manifests);
4018
+ const auditConfig = withContentReadSignal(
4019
+ "A4",
4020
+ "dependency_automation_ratio",
4021
+ () => manifests.some((file) => packageJsonHasAuditScript(repoPath, file))
4022
+ );
4023
+ const { manifest, dependencySpecs } = withContentReadSignals(
4024
+ "A4",
4025
+ ["pinned_dependency_ratio", "declared_version_range_ratio", "dependency_count_sanity"],
4026
+ () => ({
4027
+ // Cite the manifest that actually carries dependencies for evidence/findings, not
4028
+ // whichever sorts first (e.g. a near-empty requirements-windows.txt that precedes the
4029
+ // real requirements.txt), so the evidence pointer reflects the file the A4 score is
4030
+ // derived from. The score itself is unaffected: it aggregates specs across ALL manifests.
4031
+ manifest: pickPrimaryDependencyManifest(repoPath, manifests),
4032
+ dependencySpecs: readDependencySpecs(repoPath, manifests)
4033
+ })
4034
+ );
3642
4035
  const pinnedDependencyCount = dependencySpecs.filter(
3643
4036
  (dependency) => isPinnedDependencyVersion(dependency.version)
3644
4037
  ).length;
3645
- if (manifest)
3646
- evidence.push(
3647
- evidenceForRelative(repoPath, manifest, "dependency_report", "Dependency manifest")
4038
+ if (manifest) {
4039
+ withContentReadSignals(
4040
+ "A4",
4041
+ ["pinned_dependency_ratio", "declared_version_range_ratio", "dependency_count_sanity"],
4042
+ () => evidence.push(
4043
+ evidenceForRelative(repoPath, manifest, "dependency_report", "Dependency manifest")
4044
+ )
3648
4045
  );
4046
+ }
3649
4047
  if (lockfile)
3650
4048
  evidence.push(
3651
4049
  evidenceForRelative(repoPath, lockfile, "dependency_report", "Dependency lockfile")
@@ -3678,28 +4076,36 @@ function collectA4DependencyEvidence(repoPath, repoFiles, mono, useV27Detectors,
3678
4076
  const deployableServiceFiles = useV47Detectors ? findDeployableServicePremiseFiles(repoPath, repoFiles, true) : [];
3679
4077
  const isAppOrService = (useV47Detectors ? deployableServiceFiles.length > 0 : isDeployableService(repoPath, repoFiles, useV27Detectors)) || packagedApplicationFiles.length > 0;
3680
4078
  if (isAppOrService && !hasLockfile && manifest) {
3681
- findings.push(
3682
- inventoryAbsenceFinding({
3683
- severity: "critical",
3684
- summaries: {
3685
- legacy: "Dependency manifest is present without a detected lockfile \u2014 non-reproducible installs.",
3686
- currentTracked: "Dependency manifest is present, but the tracked scan-eligible inventory matched no recognized lockfile.",
3687
- currentDirectoryWalk: "Dependency manifest is present, but the bounded directory walk matched no recognized lockfile."
3688
- },
3689
- evidence: evidenceForRelative(
3690
- repoPath,
3691
- manifest,
3692
- "dependency_report",
3693
- "Dependency manifest"
3694
- ),
3695
- context: inventoryAbsenceContext,
3696
- inventoryCount: repoFiles.length + (mono?.sharedFiles.length ?? 0),
3697
- patternSet: INVENTORY_SCAN_PATTERN_SETS.a4Lockfile,
3698
- matchCount: 0
3699
- })
4079
+ withContentReadSignals(
4080
+ "A4",
4081
+ ["pinned_dependency_ratio", "declared_version_range_ratio", "dependency_count_sanity"],
4082
+ () => findings.push(
4083
+ inventoryAbsenceFinding({
4084
+ severity: "critical",
4085
+ summaries: {
4086
+ legacy: "Dependency manifest is present without a detected lockfile \u2014 non-reproducible installs.",
4087
+ currentTracked: "Dependency manifest is present, but the tracked scan-eligible inventory matched no recognized lockfile.",
4088
+ currentDirectoryWalk: "Dependency manifest is present, but the bounded directory walk matched no recognized lockfile."
4089
+ },
4090
+ evidence: evidenceForRelative(
4091
+ repoPath,
4092
+ manifest,
4093
+ "dependency_report",
4094
+ "Dependency manifest"
4095
+ ),
4096
+ context: inventoryAbsenceContext,
4097
+ inventoryCount: repoFiles.length + (mono?.sharedFiles.length ?? 0),
4098
+ patternSet: INVENTORY_SCAN_PATTERN_SETS.a4Lockfile,
4099
+ matchCount: 0
4100
+ })
4101
+ )
3700
4102
  );
3701
4103
  }
3702
- if (manifest && hasSuspiciousDependencies(repoPath, manifest, useV47Detectors)) {
4104
+ if (manifest && withContentReadSignal(
4105
+ "A4",
4106
+ "suspicious_dependencies",
4107
+ () => hasSuspiciousDependencies(repoPath, manifest, useV47Detectors)
4108
+ )) {
3703
4109
  findings.push({
3704
4110
  severity: "critical",
3705
4111
  summary: "Dependency manifest contains obviously suspicious placeholder dependency names.",
@@ -3853,16 +4259,24 @@ function collectA5ClaimRealityEvidence(repoPath, repoFiles, useV27Detectors, inv
3853
4259
  ...allDocFiles,
3854
4260
  ...repoFiles.filter((file) => /(^|\/)(SECURITY|THREAT[_-]?MODEL)\.md$/i.test(file))
3855
4261
  ]);
3856
- const negativeSpaceDoc = [...negativeSpaceCandidates].find(
3857
- (file) => fileContains(repoPath, file, NEGATIVE_SPACE_SECTION_PATTERN) || fileContains(repoPath, file, NEGATIVE_SPACE_PHRASE_PATTERN)
4262
+ const negativeSpaceDoc = withContentReadSignal(
4263
+ "A5",
4264
+ "negative_space_documentation",
4265
+ () => [...negativeSpaceCandidates].find(
4266
+ (file) => fileContains(repoPath, file, NEGATIVE_SPACE_SECTION_PATTERN) || fileContains(repoPath, file, NEGATIVE_SPACE_PHRASE_PATTERN)
4267
+ )
3858
4268
  );
3859
4269
  if (negativeSpaceDoc) {
3860
- evidence.push(
3861
- evidenceForRelative(
3862
- repoPath,
3863
- negativeSpaceDoc,
3864
- "claim_reconciliation",
3865
- 'Documented limitations / threat model / "not covered" section'
4270
+ withContentReadSignal(
4271
+ "A5",
4272
+ "negative_space_documentation",
4273
+ () => evidence.push(
4274
+ evidenceForRelative(
4275
+ repoPath,
4276
+ negativeSpaceDoc,
4277
+ "claim_reconciliation",
4278
+ 'Documented limitations / threat model / "not covered" section'
4279
+ )
3866
4280
  )
3867
4281
  );
3868
4282
  }
@@ -3999,18 +4413,33 @@ function collectB3CiDisciplineEvidence(repoPath, repoFiles, mono) {
3999
4413
  const workflows = repoFiles.filter(isCiWorkflow);
4000
4414
  const workflow = workflows[0];
4001
4415
  const rootWorkflows = mono ? mono.sharedFiles.filter(isCiWorkflow) : [];
4002
- const defaultBranchCiCount = workflows.filter((file) => workflowTargetsDefaultBranch(repoPath, file)).length + (mono ? rootWorkflows.filter((file) => workflowTargetsDefaultBranch(mono.root, file)).length : 0);
4416
+ const defaultBranchCiCount = withContentReadSignal(
4417
+ "B3",
4418
+ "default_branch_ci_depth",
4419
+ () => workflows.filter((file) => workflowTargetsDefaultBranch(repoPath, file)).length + (mono ? rootWorkflows.filter((file) => workflowTargetsDefaultBranch(mono.root, file)).length : 0)
4420
+ );
4003
4421
  if (packageJson && scripts.has("test")) {
4004
4422
  evidence.push(evidenceForRelative(repoPath, packageJson, "test_run", "Test script"));
4005
4423
  }
4006
4424
  if (packageJson && scripts.has("lint")) {
4007
4425
  evidence.push(evidenceForRelative(repoPath, packageJson, "ci_run", "Lint script"));
4008
4426
  }
4009
- if (workflow) evidence.push(evidenceForRelative(repoPath, workflow, "ci_run", "CI workflow"));
4010
- else if (rootWorkflows[0] && mono)
4011
- evidence.push(
4012
- evidenceForRelative(mono.root, rootWorkflows[0], "ci_run", "CI workflow (monorepo root)")
4427
+ if (workflow) {
4428
+ withContentReadSignals(
4429
+ "B3",
4430
+ ["ci_script_depth", "default_branch_ci_depth"],
4431
+ () => evidence.push(evidenceForRelative(repoPath, workflow, "ci_run", "CI workflow"))
4432
+ );
4433
+ } else if (rootWorkflows[0] && mono) {
4434
+ const rootWorkflow = rootWorkflows[0];
4435
+ withContentReadSignals(
4436
+ "B3",
4437
+ ["ci_script_depth", "default_branch_ci_depth"],
4438
+ () => evidence.push(
4439
+ evidenceForRelative(mono.root, rootWorkflow, "ci_run", "CI workflow (monorepo root)")
4440
+ )
4013
4441
  );
4442
+ }
4014
4443
  if (evidence.length === 0) return null;
4015
4444
  const CI_BUILD_COMMAND_PATTERN = /\b(cargo\s+build|cmake|ninja|make\s+(build|all))\b/i;
4016
4445
  const CI_COMMAND_CATEGORIES = [
@@ -4020,10 +4449,19 @@ function collectB3CiDisciplineEvidence(repoPath, repoFiles, mono) {
4020
4449
  CI_BUILD_COMMAND_PATTERN
4021
4450
  ];
4022
4451
  const scriptDepth = ["test", "lint", "typecheck", "build"].filter((s) => scripts.has(s)).length;
4023
- const countCiCommandCategories = (root, files) => CI_COMMAND_CATEGORIES.filter(
4024
- (pattern) => files.some((file) => fileContains(root, file, pattern))
4025
- ).length;
4026
- const ciCommandDepth = countCiCommandCategories(repoPath, workflows) + (mono ? countCiCommandCategories(mono.root, rootWorkflows) : 0);
4452
+ const { repoCategoryHits, rootCategoryHits } = withContentReadSignal(
4453
+ "B3",
4454
+ "ci_script_depth",
4455
+ () => ({
4456
+ repoCategoryHits: CI_COMMAND_CATEGORIES.map(
4457
+ (pattern) => workflows.some((file) => fileContains(repoPath, file, pattern))
4458
+ ),
4459
+ rootCategoryHits: mono ? CI_COMMAND_CATEGORIES.map(
4460
+ (pattern) => rootWorkflows.some((file) => fileContains(mono.root, file, pattern))
4461
+ ) : []
4462
+ })
4463
+ );
4464
+ const ciCommandDepth = repoCategoryHits.filter(Boolean).length + rootCategoryHits.filter(Boolean).length;
4027
4465
  const combinedDepth = scriptDepth + ciCommandDepth;
4028
4466
  return {
4029
4467
  criterionId: "B3",
@@ -4045,13 +4483,13 @@ function collectB3CiDisciplineEvidence(repoPath, repoFiles, mono) {
4045
4483
  label: `package script: ${name === "typecheck" ? "type-check" : name}`,
4046
4484
  count: scripts.has(name) ? 1 : 0
4047
4485
  })),
4048
- ...CI_COMMAND_CATEGORIES.map((pattern, index) => ({
4486
+ ...CI_COMMAND_CATEGORIES.map((_pattern, index) => ({
4049
4487
  label: `repository CI command: ${["test", "lint", "type-check", "build"][index]}`,
4050
- count: workflows.some((file) => fileContains(repoPath, file, pattern)) ? 1 : 0
4488
+ count: repoCategoryHits[index] ? 1 : 0
4051
4489
  })),
4052
- ...mono ? CI_COMMAND_CATEGORIES.map((pattern, index) => ({
4490
+ ...mono ? CI_COMMAND_CATEGORIES.map((_pattern, index) => ({
4053
4491
  label: `monorepo-root CI command: ${["test", "lint", "type-check", "build"][index]}`,
4054
- count: rootWorkflows.some((file) => fileContains(mono.root, file, pattern)) ? 1 : 0
4492
+ count: rootCategoryHits[index] ? 1 : 0
4055
4493
  })) : []
4056
4494
  ]
4057
4495
  }
@@ -4096,9 +4534,11 @@ function readAuditFreshnessCommitterYear(repoPath, scanLimitations) {
4096
4534
  function collectB4AuditEvidence(repoPath, repoFiles, generatedAt, useV19CommitYear = false, scanLimitations = /* @__PURE__ */ new Set()) {
4097
4535
  const evidence = [];
4098
4536
  const auditFiles = repoFiles.filter(isAuditFile);
4099
- for (const file of auditFiles.slice(0, 5)) {
4100
- evidence.push(evidenceForRelative(repoPath, file, "audit_log", "Audit or changelog artifact"));
4101
- }
4537
+ withContentReadSignal("B4", "audit_freshness_depth", () => {
4538
+ for (const file of auditFiles.slice(0, 5)) {
4539
+ evidence.push(evidenceForRelative(repoPath, file, "audit_log", "Audit or changelog artifact"));
4540
+ }
4541
+ });
4102
4542
  if (evidence.length === 0) {
4103
4543
  return buildNotApplicableSignal(
4104
4544
  "B4",
@@ -4143,7 +4583,11 @@ function collectB4AuditEvidence(repoPath, repoFiles, generatedAt, useV19CommitYe
4143
4583
  metric(
4144
4584
  "audit_freshness_depth",
4145
4585
  "Audit freshness depth",
4146
- countFilesContaining(repoPath, auditFiles, freshnessPattern),
4586
+ withContentReadSignal(
4587
+ "B4",
4588
+ "audit_freshness_depth",
4589
+ () => countFilesContaining(repoPath, auditFiles, freshnessPattern)
4590
+ ),
4147
4591
  Math.max(auditFiles.length, 1),
4148
4592
  0.2,
4149
4593
  "ratio",
@@ -4156,93 +4600,128 @@ function collectB6PrivilegedOpsGatingEvidence(repoPath, repoFiles, useV39Detecto
4156
4600
  const evidence = [];
4157
4601
  const findings = [];
4158
4602
  const docFiles = repoFiles.filter((file) => /\.(md|mdx)$/i.test(file));
4159
- const humanGateDoc = docFiles.find(
4160
- (file) => fileContains(repoPath, file, HUMAN_GATE_MARKER_PATTERN)
4603
+ const humanGateDoc = withContentReadSignal(
4604
+ "B6",
4605
+ "human_gate_documented",
4606
+ () => docFiles.find((file) => fileContains(repoPath, file, HUMAN_GATE_MARKER_PATTERN))
4161
4607
  );
4162
4608
  const implFiles = repoFiles.filter(isImplementationFile);
4163
- const gatedPrivilegeCheckFile = implFiles.find(
4164
- (file) => fileContains(repoPath, file, GATED_PRIVILEGE_CHECK_PATTERN) && fileContains(repoPath, file, SET_ROLE_PATTERN)
4609
+ const gatedPrivilegeCheckFile = withContentReadSignal(
4610
+ "B6",
4611
+ "fail_closed_privilege_check",
4612
+ () => implFiles.find(
4613
+ (file) => fileContains(repoPath, file, GATED_PRIVILEGE_CHECK_PATTERN) && fileContains(repoPath, file, SET_ROLE_PATTERN)
4614
+ )
4165
4615
  );
4166
- const killSwitchFile = implFiles.find(
4167
- (file) => fileContains(repoPath, file, KILL_SWITCH_NAME_PATTERN) && fileContains(repoPath, file, KILL_SWITCH_FAIL_CLOSED_PATTERN)
4616
+ const killSwitchFile = withContentReadSignal(
4617
+ "B6",
4618
+ "kill_switch_fail_safe_present",
4619
+ () => implFiles.find(
4620
+ (file) => fileContains(repoPath, file, KILL_SWITCH_NAME_PATTERN) && fileContains(repoPath, file, KILL_SWITCH_FAIL_CLOSED_PATTERN)
4621
+ )
4168
4622
  );
4169
4623
  const codeownersFile = repoFiles.find((file) => /(^|\/)CODEOWNERS$/.test(file));
4170
- const reviewGateDoc = docFiles.find(
4171
- (file) => fileContains(repoPath, file, REQUIRED_REVIEW_PATTERN)
4624
+ const reviewGateDoc = withContentReadSignal(
4625
+ "B6",
4626
+ "protected_path_review_gate",
4627
+ () => docFiles.find((file) => fileContains(repoPath, file, REQUIRED_REVIEW_PATTERN))
4172
4628
  );
4173
4629
  const protectedPathReviewGate = codeownersFile ?? reviewGateDoc;
4174
- const executableFiles = implFiles.filter(
4175
- (file) => fileContains(repoPath, file, SQL_EXEC_PATTERN)
4176
- );
4177
- const historicalUngatedEscalationFiles = executableFiles.filter((file) => {
4178
- if (useV39Detectors && !fileHasExecutedPrivilegeEscalation(repoPath, file)) return false;
4179
- const hasEscalation = fileContains(repoPath, file, ROLE_MEMBERSHIP_GRANT_PATTERN) || fileContains(repoPath, file, SUPERUSER_ESCALATION_PATTERN);
4180
- if (!hasEscalation) return false;
4181
- return !fileContains(repoPath, file, HUMAN_GATE_MARKER_PATTERN);
4630
+ const { executableFiles, v21ExecutedEscalationFileSet, productionUngatedEscalationFiles } = withContentReadSignal("B6", "privilege_escalation_cleanliness", () => {
4631
+ const executable = implFiles.filter((file) => fileContains(repoPath, file, SQL_EXEC_PATTERN));
4632
+ const historicalUngated = executable.filter((file) => {
4633
+ if (useV39Detectors && !fileHasExecutedPrivilegeEscalation(repoPath, file)) return false;
4634
+ const hasEscalation = fileContains(repoPath, file, ROLE_MEMBERSHIP_GRANT_PATTERN) || fileContains(repoPath, file, SUPERUSER_ESCALATION_PATTERN);
4635
+ if (!hasEscalation) return false;
4636
+ return !fileContains(repoPath, file, HUMAN_GATE_MARKER_PATTERN);
4637
+ });
4638
+ const v21Executed = useV21ExecutedEscalations ? repoFiles.filter(
4639
+ (file) => isV39AuthoredProductionPath(file) && (fileHasV21RawSqlEscalation(repoPath, file) || isImplementationFile(file) && fileHasV21DriverEscalation(repoPath, file))
4640
+ ) : [];
4641
+ const v21ExecutedSet = new Set(v21Executed);
4642
+ const ungated = [.../* @__PURE__ */ new Set([...historicalUngated, ...v21Executed])].filter(
4643
+ (file) => !fileContains(repoPath, file, HUMAN_GATE_MARKER_PATTERN)
4644
+ );
4645
+ const productionUngated = ungated.filter(
4646
+ (file) => useV39Detectors ? isV39AuthoredProductionPath(file) : !isTestOrFixturePath(file)
4647
+ );
4648
+ return {
4649
+ executableFiles: executable,
4650
+ v21ExecutedEscalationFileSet: v21ExecutedSet,
4651
+ productionUngatedEscalationFiles: productionUngated
4652
+ };
4182
4653
  });
4183
- const v21ExecutedEscalationFiles = useV21ExecutedEscalations ? repoFiles.filter(
4184
- (file) => isV39AuthoredProductionPath(file) && (fileHasV21RawSqlEscalation(repoPath, file) || isImplementationFile(file) && fileHasV21DriverEscalation(repoPath, file))
4185
- ) : [];
4186
- const v21ExecutedEscalationFileSet = new Set(v21ExecutedEscalationFiles);
4187
- const ungatedEscalationFiles = [
4188
- .../* @__PURE__ */ new Set([...historicalUngatedEscalationFiles, ...v21ExecutedEscalationFiles])
4189
- ].filter((file) => !fileContains(repoPath, file, HUMAN_GATE_MARKER_PATTERN));
4190
- const productionUngatedEscalationFiles = ungatedEscalationFiles.filter(
4191
- (file) => useV39Detectors ? isV39AuthoredProductionPath(file) : !isTestOrFixturePath(file)
4192
- );
4193
4654
  if (humanGateDoc) {
4194
- evidence.push(
4195
- evidenceForRelative(
4196
- repoPath,
4197
- humanGateDoc,
4198
- "artifact",
4199
- "Documents privileged operations as human-executed/gated"
4655
+ withContentReadSignal(
4656
+ "B6",
4657
+ "human_gate_documented",
4658
+ () => evidence.push(
4659
+ evidenceForRelative(
4660
+ repoPath,
4661
+ humanGateDoc,
4662
+ "artifact",
4663
+ "Documents privileged operations as human-executed/gated"
4664
+ )
4200
4665
  )
4201
4666
  );
4202
4667
  }
4203
4668
  if (gatedPrivilegeCheckFile) {
4204
- evidence.push(
4205
- evidenceForRelative(
4206
- repoPath,
4207
- gatedPrivilegeCheckFile,
4208
- "artifact",
4209
- "Fail-closed privilege-membership check before role elevation"
4669
+ withContentReadSignal(
4670
+ "B6",
4671
+ "fail_closed_privilege_check",
4672
+ () => evidence.push(
4673
+ evidenceForRelative(
4674
+ repoPath,
4675
+ gatedPrivilegeCheckFile,
4676
+ "artifact",
4677
+ "Fail-closed privilege-membership check before role elevation"
4678
+ )
4210
4679
  )
4211
4680
  );
4212
4681
  }
4213
4682
  if (killSwitchFile) {
4214
- evidence.push(
4215
- evidenceForRelative(
4216
- repoPath,
4217
- killSwitchFile,
4218
- "artifact",
4219
- "Un-overridable kill-switch / fail-safe governance toggle"
4683
+ withContentReadSignal(
4684
+ "B6",
4685
+ "kill_switch_fail_safe_present",
4686
+ () => evidence.push(
4687
+ evidenceForRelative(
4688
+ repoPath,
4689
+ killSwitchFile,
4690
+ "artifact",
4691
+ "Un-overridable kill-switch / fail-safe governance toggle"
4692
+ )
4220
4693
  )
4221
4694
  );
4222
4695
  }
4223
4696
  if (protectedPathReviewGate) {
4224
- evidence.push(
4225
- evidenceForRelative(
4226
- repoPath,
4227
- protectedPathReviewGate,
4228
- "artifact",
4229
- "CODEOWNERS/required-review gate on protected paths"
4697
+ withContentReadSignal(
4698
+ "B6",
4699
+ "protected_path_review_gate",
4700
+ () => evidence.push(
4701
+ evidenceForRelative(
4702
+ repoPath,
4703
+ protectedPathReviewGate,
4704
+ "artifact",
4705
+ "CODEOWNERS/required-review gate on protected paths"
4706
+ )
4230
4707
  )
4231
4708
  );
4232
4709
  }
4233
- for (const file of productionUngatedEscalationFiles.slice(0, 5)) {
4234
- const isV21ExecutedShape = v21ExecutedEscalationFileSet.has(file);
4235
- findings.push({
4236
- severity: "critical",
4237
- summary: isV21ExecutedShape ? "This file contains an authored administrative SQL statement \u2014 role-membership grant, SUPERUSER escalation, or schema-wide table privilege grant \u2014 or executes one through a direct database-driver call, and does not itself contain text matching the human-gate marker pattern." : "This file executes a role-membership GRANT or SUPERUSER escalation and does not itself contain text matching the human-gate marker pattern.",
4238
- evidence: evidenceForRelative(
4239
- repoPath,
4240
- file,
4241
- "artifact",
4242
- isV21ExecutedShape ? "Ungated authored or directly executed administrative SQL statement" : "Ungated privilege-escalation statement"
4243
- )
4244
- });
4245
- }
4710
+ withContentReadSignal("B6", "privilege_escalation_cleanliness", () => {
4711
+ for (const file of productionUngatedEscalationFiles.slice(0, 5)) {
4712
+ const isV21ExecutedShape = v21ExecutedEscalationFileSet.has(file);
4713
+ findings.push({
4714
+ severity: "critical",
4715
+ summary: isV21ExecutedShape ? "This file contains an authored administrative SQL statement \u2014 role-membership grant, SUPERUSER escalation, or schema-wide table privilege grant \u2014 or executes one through a direct database-driver call, and does not itself contain text matching the human-gate marker pattern." : "This file executes a role-membership GRANT or SUPERUSER escalation and does not itself contain text matching the human-gate marker pattern.",
4716
+ evidence: evidenceForRelative(
4717
+ repoPath,
4718
+ file,
4719
+ "artifact",
4720
+ isV21ExecutedShape ? "Ungated authored or directly executed administrative SQL statement" : "Ungated privilege-escalation statement"
4721
+ )
4722
+ });
4723
+ }
4724
+ });
4246
4725
  if (evidence.length === 0 && findings.length === 0) {
4247
4726
  return buildNotApplicableSignal(
4248
4727
  "B6",
@@ -4264,7 +4743,7 @@ function collectB6PrivilegedOpsGatingEvidence(repoPath, repoFiles, useV39Detecto
4264
4743
  }
4265
4744
  )
4266
4745
  ] : [];
4267
- const hasPrivilegedOpsSurface = humanGateDoc != null || gatedPrivilegeCheckFile != null || executableFiles.length > 0 || v21ExecutedEscalationFiles.length > 0;
4746
+ const hasPrivilegedOpsSurface = humanGateDoc != null || gatedPrivilegeCheckFile != null || executableFiles.length > 0 || v21ExecutedEscalationFileSet.size > 0;
4268
4747
  return {
4269
4748
  criterionId: "B6",
4270
4749
  positiveEvidence: evidence,
@@ -4408,6 +4887,7 @@ var GATED_PRIVILEGE_CHECK_PATTERN = /pg_has_role\s*\(|has_role\s*\(|is not a mem
4408
4887
  var SET_ROLE_PATTERN = /\bset\s+(local\s+)?role\b/i;
4409
4888
  var REQUIRED_REVIEW_PATTERN = /required review|branch protection|protected branch/i;
4410
4889
  var HISTORY_SECRET_SCAN_CREDENTIAL_BLOB_LIMIT = 5e3;
4890
+ var V23_PEM_WIDENED_HISTORY_BLOB_LIMIT = 500;
4411
4891
  var SECRET_IDENTIFIER_KEYWORD_PATTERN = /secret|token|api[_-]?key|password|access[_-]?key/gi;
4412
4892
  var SINGLE_WHITESPACE_PATTERN = /\s/u;
4413
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,})$/;
@@ -4567,12 +5047,7 @@ function parseGitTrackedFiles(repoPath, output, includeHardExcluded) {
4567
5047
  continue;
4568
5048
  }
4569
5049
  if (stat.size > MAX_REPOSITORY_CONTENT_BYTES) {
4570
- const affectedCriteria = affectedCriteriaForUnavailablePath(file);
4571
- if (affectedCriteria.length > 0) {
4572
- recordContentSkipForCriteria(fullPath, "too_large", affectedCriteria, true);
4573
- } else {
4574
- recordContentSkip(fullPath, "too_large", true);
4575
- }
5050
+ recordContentSkip(fullPath, "too_large", true);
4576
5051
  continue;
4577
5052
  }
4578
5053
  } catch (error) {
@@ -4586,39 +5061,6 @@ function parseGitTrackedFiles(repoPath, output, includeHardExcluded) {
4586
5061
  }
4587
5062
  return files.sort();
4588
5063
  }
4589
- function affectedCriteriaForUnavailablePath(file) {
4590
- const affected = /* @__PURE__ */ new Set();
4591
- if (isDependencyManifest(file) || isLockfile(file)) affected.add("A4");
4592
- if (isCiWorkflow(file)) {
4593
- affected.add("A1");
4594
- affected.add("A3");
4595
- affected.add("B2");
4596
- affected.add("B3");
4597
- }
4598
- if (isDeployConfig(file)) {
4599
- affected.add("A3");
4600
- affected.add("B2");
4601
- }
4602
- if (isTestFile(file)) {
4603
- affected.add("A1");
4604
- affected.add("A2");
4605
- }
4606
- if (isImplementationFile(file)) {
4607
- affected.add("A1");
4608
- affected.add("A2");
4609
- affected.add("A3");
4610
- affected.add("B1");
4611
- affected.add("B6");
4612
- }
4613
- if (/\.(?:md|mdx|rst|txt)$/i.test(file)) {
4614
- affected.add("A5");
4615
- affected.add("B4");
4616
- affected.add("B5");
4617
- affected.add("B6");
4618
- }
4619
- if (isAuditFile(file)) affected.add("B5");
4620
- return [...affected];
4621
- }
4622
5064
  function directoryInventory(repoPath) {
4623
5065
  const files = [];
4624
5066
  const rootDevice = statSync(repoPath).dev;
@@ -4740,12 +5182,7 @@ function visitRepoDir(repoPath, dirPath, files, rootDevice) {
4740
5182
  }
4741
5183
  const repoRelativePath = relative(repoPath, fullPath);
4742
5184
  if (size > MAX_REPOSITORY_CONTENT_BYTES) {
4743
- const affectedCriteria = affectedCriteriaForUnavailablePath(repoRelativePath);
4744
- if (affectedCriteria.length > 0) {
4745
- recordContentSkipForCriteria(fullPath, "too_large", affectedCriteria);
4746
- } else {
4747
- recordContentSkip(fullPath, "too_large");
4748
- }
5185
+ recordContentSkip(fullPath, "too_large");
4749
5186
  continue;
4750
5187
  }
4751
5188
  if (!isPotentialContentPath(repoRelativePath)) {
@@ -5187,6 +5624,9 @@ function isRecord(value) {
5187
5624
  function isTestRunnerConfig(repoPath, file) {
5188
5625
  return /(^|\/)(vitest|jest|pytest|playwright|cypress|mocha)\.config\.[cm]?[jt]s$/.test(file) || /(^|\/)(pytest\.ini|tox\.ini)$/.test(file) || /(^|\/)phpunit\.xml(\.dist)?$/.test(file) || /(^|\/)pyproject\.toml$/.test(file) && fileContains(repoPath, file, /^\[tool\.pytest(?:\.|\])/m) || /(^|\/)CMakeLists\.txt$/.test(file) && fileContains(repoPath, file, /\b(?:enable_testing|add_test|ctest_add_tests)\s*\(/i) || /(^|\/)Makefile$/.test(file) && fileContains(repoPath, file, /^\s*(?:test|tests|check)\s*:/m);
5189
5626
  }
5627
+ function isRecipeTestEntry(repoPath, file) {
5628
+ return isRecipeFile(file) && fileContains(repoPath, file, /^\s*(?:test|tests|check)\s*:/m);
5629
+ }
5190
5630
  function findCoverageConfigFiles(repoPath, repoFiles, authoredOnly = false) {
5191
5631
  return repoFiles.filter(
5192
5632
  (file) => (!authoredOnly || isAuthoredProductionPath(file)) && isCoverageConfig(repoPath, file)
@@ -5480,6 +5920,580 @@ function packageJsonHasCoverageTooling(repoPath, file) {
5480
5920
  }
5481
5921
  return false;
5482
5922
  }
5923
+ function isV23CoverageConfig(repoPath, file) {
5924
+ if (/(^|\/)vitest\.config\.[cm]?[jt]s$/.test(file)) {
5925
+ const contents = stripJavaScriptComments(readRepoText(join(repoPath, file), "utf8"));
5926
+ const property = /(?:^|[{,])\s*["']?coverage["']?\s*:\s*([^,\r\n}]*)/im.exec(contents)?.[1]?.trim();
5927
+ if (!property || /^(?:false\b|null\b|undefined\b)/i.test(property)) return false;
5928
+ if (/^\{/.test(property) && /\benabled\s*:\s*false\b/i.test(contents)) return false;
5929
+ return true;
5930
+ }
5931
+ if (/(^|\/)jest\.config\.[cm]?[jt]s$/.test(file)) {
5932
+ const contents = stripJavaScriptComments(readRepoText(join(repoPath, file), "utf8"));
5933
+ return /(?:^|[{,])\s*["']?collectCoverage["']?\s*:\s*true\b/m.test(contents) || /(?:^|[{,])\s*["']?(?:coverageThreshold|coverageProvider|coverageDirectory)["']?\s*:/m.test(
5934
+ contents
5935
+ );
5936
+ }
5937
+ if (/(^|\/)package\.json$/.test(file)) {
5938
+ return packageJsonHasStaticCoverageTooling(repoPath, file);
5939
+ }
5940
+ return isCoverageConfig(repoPath, file);
5941
+ }
5942
+ function coverageConfigurationDecision(repoPath, file) {
5943
+ const detectorState = isV23CoverageConfig(repoPath, file) ? "collects" : "none";
5944
+ if (/(^|\/)vitest\.config\.[cm]?[jt]s$/.test(file)) {
5945
+ const rawContents = readRepoText(join(repoPath, file), "utf8");
5946
+ const contents = stripJavaScriptComments(rawContents);
5947
+ const match = /(?:^|[{,])\s*["']?coverage["']?\s*:\s*([^,\r\n}]*)/im.exec(contents);
5948
+ if (!match) return null;
5949
+ return {
5950
+ path: file,
5951
+ source: "runner-configuration",
5952
+ locator: "coverage",
5953
+ evidence: rawContents.slice(match.index, match.index + match[0].length).trim(),
5954
+ detectorRecognized: true,
5955
+ detectorState
5956
+ };
5957
+ }
5958
+ if (/(^|\/)jest\.config\.[cm]?[jt]s$/.test(file)) {
5959
+ const rawContents = readRepoText(join(repoPath, file), "utf8");
5960
+ const contents = stripJavaScriptComments(rawContents);
5961
+ const match = /(?:^|[{,])\s*["']?(collectCoverage|coverageThreshold|coverageProvider|coverageDirectory)["']?\s*:\s*([^,\r\n}]*)/m.exec(
5962
+ contents
5963
+ );
5964
+ if (!match) return null;
5965
+ return {
5966
+ path: file,
5967
+ source: "runner-configuration",
5968
+ locator: match[1] ?? "coverage",
5969
+ evidence: rawContents.slice(match.index, match.index + match[0].length).trim(),
5970
+ detectorRecognized: true,
5971
+ detectorState
5972
+ };
5973
+ }
5974
+ if (detectorState === "none") return null;
5975
+ return {
5976
+ path: file,
5977
+ source: "runner-configuration",
5978
+ locator: "coverage",
5979
+ evidence: file,
5980
+ detectorRecognized: true,
5981
+ detectorState
5982
+ };
5983
+ }
5984
+ function packageJsonHasStaticCoverageTooling(repoPath, file) {
5985
+ const fullPath = join(repoPath, file);
5986
+ if (!isRegularFile(fullPath)) return false;
5987
+ const parsed = parseJsonObject(readRepoText(fullPath, "utf8"));
5988
+ if (!parsed) return false;
5989
+ return ["nyc", "c8", "istanbul"].some((key) => {
5990
+ const config = parsed[key];
5991
+ return typeof config === "object" && config !== null && !Array.isArray(config);
5992
+ });
5993
+ }
5994
+ function analyzeCoverageConfiguration(repoPath, repoFiles, authoredOnly = false) {
5995
+ const eligibleFiles = repoFiles.filter(
5996
+ (file) => !authoredOnly || isAuthoredProductionPath(file)
5997
+ );
5998
+ const eligibleSet = new Set(eligibleFiles);
5999
+ const coverageFiles = /* @__PURE__ */ new Set();
6000
+ const unresolvedFiles = /* @__PURE__ */ new Set();
6001
+ const ciCommandsByFile = /* @__PURE__ */ new Map();
6002
+ const detectorDecisions = [];
6003
+ let hasCoverageCommand = false;
6004
+ for (const file of eligibleFiles.filter(isCiWorkflow)) {
6005
+ const commands = extractCiRunCommands(readRepoText(join(repoPath, file), "utf8"));
6006
+ ciCommandsByFile.set(file, commands);
6007
+ const commandDecisions = commands.map(
6008
+ (command, index) => coverageDetectorDecision(repoPath, eligibleSet, {
6009
+ path: file,
6010
+ source: "ci-run",
6011
+ locator: String(index),
6012
+ command
6013
+ })
6014
+ );
6015
+ detectorDecisions.push(...commandDecisions);
6016
+ const state = combineCoverageCommandStates(
6017
+ commandDecisions.map((decision) => decision.detectorState)
6018
+ );
6019
+ if (state === "collects") {
6020
+ coverageFiles.add(file);
6021
+ hasCoverageCommand = true;
6022
+ }
6023
+ if (state === "unresolved") unresolvedFiles.add(file);
6024
+ }
6025
+ const ciCommands = [...ciCommandsByFile.values()].flat();
6026
+ for (const file of eligibleFiles) {
6027
+ if (isCiWorkflow(file) || isRecipeFile(file)) continue;
6028
+ if (/(^|\/)package\.json$/.test(file)) {
6029
+ if (packageJsonHasStaticCoverageTooling(repoPath, file)) coverageFiles.add(file);
6030
+ const packageState = packageScriptCoverageState(
6031
+ repoPath,
6032
+ eligibleSet,
6033
+ file,
6034
+ file === "package.json" ? ciCommands : [],
6035
+ detectorDecisions
6036
+ );
6037
+ if (packageState === "collects") {
6038
+ coverageFiles.add(file);
6039
+ hasCoverageCommand = true;
6040
+ }
6041
+ if (packageState === "unresolved") unresolvedFiles.add(file);
6042
+ continue;
6043
+ }
6044
+ const configurationDecision = coverageConfigurationDecision(repoPath, file);
6045
+ if (configurationDecision) detectorDecisions.push(configurationDecision);
6046
+ if (configurationDecision?.detectorState === "collects") coverageFiles.add(file);
6047
+ }
6048
+ for (const file of eligibleFiles.filter(isRecipeFile)) {
6049
+ const state = recipeCoverageState(
6050
+ repoPath,
6051
+ eligibleSet,
6052
+ file,
6053
+ ciCommands,
6054
+ detectorDecisions
6055
+ );
6056
+ if (state === "collects") {
6057
+ coverageFiles.add(file);
6058
+ hasCoverageCommand = true;
6059
+ }
6060
+ if (state === "unresolved") unresolvedFiles.add(file);
6061
+ }
6062
+ for (const file of coverageFiles) unresolvedFiles.delete(file);
6063
+ return {
6064
+ coverageFiles: [...coverageFiles].sort(),
6065
+ unresolvedFiles: [...unresolvedFiles].sort(),
6066
+ hasCoverageCommand,
6067
+ detectorDecisions
6068
+ };
6069
+ }
6070
+ function stripJavaScriptComments(contents) {
6071
+ let output = "";
6072
+ let quote = null;
6073
+ let escaped = false;
6074
+ let lineComment = false;
6075
+ let blockComment = false;
6076
+ for (let index = 0; index < contents.length; index += 1) {
6077
+ const character = contents[index] ?? "";
6078
+ const next = contents[index + 1] ?? "";
6079
+ if (lineComment) {
6080
+ if (character === "\n") {
6081
+ lineComment = false;
6082
+ output += "\n";
6083
+ } else {
6084
+ output += " ";
6085
+ }
6086
+ continue;
6087
+ }
6088
+ if (blockComment) {
6089
+ if (character === "*" && next === "/") {
6090
+ output += " ";
6091
+ index += 1;
6092
+ blockComment = false;
6093
+ } else {
6094
+ output += character === "\n" ? "\n" : " ";
6095
+ }
6096
+ continue;
6097
+ }
6098
+ if (escaped) {
6099
+ output += character;
6100
+ escaped = false;
6101
+ continue;
6102
+ }
6103
+ if (quote) {
6104
+ output += character;
6105
+ if (character === "\\") escaped = true;
6106
+ else if (character === quote) quote = null;
6107
+ continue;
6108
+ }
6109
+ if (character === "'" || character === '"' || character === "`") {
6110
+ quote = character;
6111
+ output += character;
6112
+ continue;
6113
+ }
6114
+ if (character === "/" && next === "/") {
6115
+ output += " ";
6116
+ index += 1;
6117
+ lineComment = true;
6118
+ continue;
6119
+ }
6120
+ if (character === "/" && next === "*") {
6121
+ output += " ";
6122
+ index += 1;
6123
+ blockComment = true;
6124
+ continue;
6125
+ }
6126
+ output += character;
6127
+ }
6128
+ return output;
6129
+ }
6130
+ function packageScriptCoverageState(repoPath, repoFiles, packageJson, ciCommands, detectorDecisions) {
6131
+ const scripts = readPackageScripts(join(repoPath, packageJson));
6132
+ if (scripts.size === 0) return "none";
6133
+ const roots = /* @__PURE__ */ new Set();
6134
+ let unresolved = false;
6135
+ if (scripts.has("test")) roots.add("test");
6136
+ for (const command of ciCommands) {
6137
+ const references = packageScriptReferences(command, scripts);
6138
+ for (const name of references.names) roots.add(name);
6139
+ if (references.dynamic && [...scripts.values()].some(
6140
+ (candidate) => commandInvokesCoverageTool(candidate) || containsPotentialCoverageFlag(candidate)
6141
+ )) {
6142
+ unresolved = true;
6143
+ }
6144
+ }
6145
+ const queue = [...roots];
6146
+ const visited = /* @__PURE__ */ new Set();
6147
+ while (queue.length > 0) {
6148
+ const name = queue.shift();
6149
+ if (!name || visited.has(name)) continue;
6150
+ const command = scripts.get(name);
6151
+ if (command === void 0) continue;
6152
+ visited.add(name);
6153
+ const decision = coverageDetectorDecision(repoPath, repoFiles, {
6154
+ path: packageJson,
6155
+ source: "package-script",
6156
+ locator: name,
6157
+ command
6158
+ });
6159
+ detectorDecisions.push(decision);
6160
+ const state = decision.detectorState;
6161
+ if (state === "collects") return "collects";
6162
+ if (state === "unresolved") unresolved = true;
6163
+ const references = packageScriptReferences(command, scripts);
6164
+ for (const referenced of references.names) {
6165
+ if (!visited.has(referenced)) queue.push(referenced);
6166
+ }
6167
+ for (const lifecycle of [`pre${name}`, `post${name}`]) {
6168
+ if (scripts.has(lifecycle) && !visited.has(lifecycle)) queue.push(lifecycle);
6169
+ }
6170
+ if (references.dynamic && [...scripts.values()].some(
6171
+ (candidate) => commandInvokesCoverageTool(candidate) || containsPotentialCoverageFlag(candidate)
6172
+ )) {
6173
+ unresolved = true;
6174
+ }
6175
+ }
6176
+ return unresolved ? "unresolved" : "none";
6177
+ }
6178
+ function packageScriptReferences(command, scripts) {
6179
+ const names = /* @__PURE__ */ new Set();
6180
+ let dynamic = false;
6181
+ for (const segment of splitShellCommandSegments(command)) {
6182
+ for (const match of segment.matchAll(
6183
+ /\bpnpm\s+run\s+\/((?:\\.|[^/])+)\/([dgimsuvy]*)/giu
6184
+ )) {
6185
+ const source = match[1];
6186
+ if (!source) continue;
6187
+ try {
6188
+ const flags = (match[2] ?? "").replace(/[dgy]/g, "");
6189
+ const selector = new RegExp(source, flags);
6190
+ for (const name of scripts.keys()) {
6191
+ if (selector.test(name)) names.add(name);
6192
+ selector.lastIndex = 0;
6193
+ }
6194
+ } catch {
6195
+ dynamic = true;
6196
+ }
6197
+ }
6198
+ for (const match of segment.matchAll(
6199
+ /\b(?:npm|pnpm|yarn)\s+(?:run\s+)?([A-Za-z0-9][A-Za-z0-9:._-]*)/giu
6200
+ )) {
6201
+ const name = match[1];
6202
+ if (name && scripts.has(name)) names.add(name);
6203
+ }
6204
+ if (/\b(?:npm|pnpm|yarn)\s+run\s+(?:\$|["']\$|`)/u.test(segment)) dynamic = true;
6205
+ }
6206
+ return { names: [...names], dynamic };
6207
+ }
6208
+ function extractCiRunCommands(contents) {
6209
+ const lines = contents.split(/\r?\n/);
6210
+ const commands = [];
6211
+ for (let index = 0; index < lines.length; index += 1) {
6212
+ const line = lines[index] ?? "";
6213
+ const match = /^(\s*)(?:-\s*)?run\s*:\s*(.*)$/u.exec(line);
6214
+ if (!match) continue;
6215
+ const indentation = match[1]?.length ?? 0;
6216
+ const value = match[2]?.trim() ?? "";
6217
+ if (value === "|" || value === ">" || value.startsWith("|-") || value.startsWith(">-")) {
6218
+ const block = [];
6219
+ for (index += 1; index < lines.length; index += 1) {
6220
+ const nested = lines[index] ?? "";
6221
+ if (nested.trim().length === 0) {
6222
+ block.push("");
6223
+ continue;
6224
+ }
6225
+ const nestedIndentation = /^\s*/u.exec(nested)?.[0].length ?? 0;
6226
+ if (nestedIndentation <= indentation) {
6227
+ index -= 1;
6228
+ break;
6229
+ }
6230
+ block.push(nested.slice(Math.min(nested.length, indentation + 2)));
6231
+ }
6232
+ commands.push(block.join(value.startsWith(">") ? " " : "\n"));
6233
+ continue;
6234
+ }
6235
+ if (value.length === 0) continue;
6236
+ const quoted = /^(?:"([\s\S]*)"|'([\s\S]*)')$/u.exec(value);
6237
+ commands.push(quoted ? quoted[1] ?? quoted[2] ?? "" : value);
6238
+ }
6239
+ return commands;
6240
+ }
6241
+ function isRecipeFile(file) {
6242
+ return /(^|\/)(?:Makefile|makefile|GNUmakefile|[Jj]ustfile)$/.test(file);
6243
+ }
6244
+ function recipeCoverageState(repoPath, repoFiles, file, ciCommands, detectorDecisions) {
6245
+ const targets = readRecipeTargets(readRepoText(join(repoPath, file), "utf8"));
6246
+ const roots = new Set(["test", "tests", "check"].filter((target) => targets.has(target)));
6247
+ const runner = /[Jj]ustfile$/.test(file) ? "just" : "make";
6248
+ for (const command of ciCommands) {
6249
+ for (const match of command.matchAll(new RegExp(`\\b${runner}\\s+([A-Za-z0-9._-]+)`, "gu"))) {
6250
+ const target = match[1];
6251
+ if (target && targets.has(target)) roots.add(target);
6252
+ }
6253
+ }
6254
+ const queue = [...roots];
6255
+ const visited = /* @__PURE__ */ new Set();
6256
+ let unresolved = false;
6257
+ while (queue.length > 0) {
6258
+ const name = queue.shift();
6259
+ if (!name || visited.has(name)) continue;
6260
+ const target = targets.get(name);
6261
+ if (!target) continue;
6262
+ visited.add(name);
6263
+ const commandDecisions = target.commands.map(
6264
+ (command, index) => coverageDetectorDecision(repoPath, repoFiles, {
6265
+ path: file,
6266
+ source: "recipe-command",
6267
+ locator: `${name}:${index}`,
6268
+ command
6269
+ })
6270
+ );
6271
+ detectorDecisions.push(...commandDecisions);
6272
+ const state = combineCoverageCommandStates(
6273
+ commandDecisions.map((decision) => decision.detectorState)
6274
+ );
6275
+ if (state === "collects") return "collects";
6276
+ if (state === "unresolved") unresolved = true;
6277
+ for (const dependency of target.dependencies) {
6278
+ if (targets.has(dependency) && !visited.has(dependency)) queue.push(dependency);
6279
+ }
6280
+ for (const command of target.commands) {
6281
+ const pattern = new RegExp(`(?:\\$\\(MAKE\\)|\\b${runner})\\s+([A-Za-z0-9._-]+)`, "gu");
6282
+ for (const match of command.matchAll(pattern)) {
6283
+ const referenced = match[1];
6284
+ if (referenced && targets.has(referenced) && !visited.has(referenced)) queue.push(referenced);
6285
+ }
6286
+ }
6287
+ }
6288
+ return unresolved ? "unresolved" : "none";
6289
+ }
6290
+ function readRecipeTargets(contents) {
6291
+ const targets = /* @__PURE__ */ new Map();
6292
+ let active = null;
6293
+ for (const line of contents.split(/\r?\n/)) {
6294
+ const header = /^([A-Za-z0-9][A-Za-z0-9._-]*)\s*:\s*([^#]*)/u.exec(line);
6295
+ if (header) {
6296
+ const name = header[1];
6297
+ if (!name) continue;
6298
+ active = {
6299
+ dependencies: (header[2] ?? "").trim().split(/\s+/u).filter(Boolean),
6300
+ commands: []
6301
+ };
6302
+ targets.set(name, active);
6303
+ continue;
6304
+ }
6305
+ if (active && /^\s+/u.test(line) && line.trim().length > 0) {
6306
+ active.commands.push(line.trim().replace(/^[@+-]+/u, ""));
6307
+ continue;
6308
+ }
6309
+ if (line.trim().length > 0 && !line.trimStart().startsWith("#")) active = null;
6310
+ }
6311
+ return targets;
6312
+ }
6313
+ function coverageCommandEvaluation(repoPath, repoFiles, command, depth = 0) {
6314
+ if (depth > 4) {
6315
+ const recognized = containsPotentialCoverageFlag(command);
6316
+ return { recognized, state: recognized ? "unresolved" : "none" };
6317
+ }
6318
+ const normalized = normalizeCoverageCommandPrefixes(command);
6319
+ const segments = normalized.splitShellOperators ? splitShellCommandSegments(normalized.segment) : [normalized.segment];
6320
+ const evaluations = segments.map((rawSegment) => {
6321
+ const candidate = normalizeCoverageCommandPrefixes(rawSegment);
6322
+ if (commandInvokesCoverageTool(candidate.segment)) {
6323
+ return { recognized: true, state: "collects" };
6324
+ }
6325
+ const words = shellWords(candidate.segment);
6326
+ if (words.length === 0) return { recognized: false, state: "none" };
6327
+ const direct = coverageFlagCommandState(words);
6328
+ if (direct !== "none") return { recognized: true, state: direct };
6329
+ const hasCoverageSyntax = containsPotentialCoverageFlag(candidate.segment);
6330
+ if (isRecognizedCoverageFlagRunner(words)) {
6331
+ return { recognized: hasCoverageSyntax, state: "none" };
6332
+ }
6333
+ const wrapper = reachableShellWrapper(words, repoFiles);
6334
+ if (wrapper) {
6335
+ return coverageCommandEvaluation(
6336
+ repoPath,
6337
+ repoFiles,
6338
+ readRepoText(join(repoPath, wrapper), "utf8"),
6339
+ depth + 1
6340
+ );
6341
+ }
6342
+ const executable = commandExecutable(words);
6343
+ const potentialFlagState = flagState(
6344
+ words.slice(1),
6345
+ ["--experimental-test-coverage", "--collectcoverage", "--collect-coverage", "--coverage", "--cov"],
6346
+ [
6347
+ "--no-experimental-test-coverage",
6348
+ "--no-collectcoverage",
6349
+ "--no-collect-coverage",
6350
+ "--no-coverage",
6351
+ "--no-cov"
6352
+ ]
6353
+ );
6354
+ if (hasCoverageSyntax && potentialFlagState !== "none" && executable && /(?:test|spec|runner)/i.test(executable) && !/(?:docs?|map|report|upload|codecov|coveralls)/i.test(executable)) {
6355
+ return { recognized: true, state: "unresolved" };
6356
+ }
6357
+ return { recognized: false, state: "none" };
6358
+ });
6359
+ return {
6360
+ recognized: evaluations.some((evaluation) => evaluation.recognized),
6361
+ state: combineCoverageCommandStates(
6362
+ evaluations.map((evaluation) => evaluation.state)
6363
+ )
6364
+ };
6365
+ }
6366
+ function coverageDetectorDecision(repoPath, repoFiles, input) {
6367
+ const evaluation = coverageCommandEvaluation(repoPath, repoFiles, input.command);
6368
+ return {
6369
+ path: input.path,
6370
+ source: input.source,
6371
+ locator: input.locator,
6372
+ evidence: input.command,
6373
+ detectorRecognized: evaluation.recognized,
6374
+ detectorState: evaluation.state
6375
+ };
6376
+ }
6377
+ function combineCoverageCommandStates(states) {
6378
+ if (states.includes("collects")) return "collects";
6379
+ return states.includes("unresolved") ? "unresolved" : "none";
6380
+ }
6381
+ function shellWords(segment) {
6382
+ const words = [];
6383
+ let offset = 0;
6384
+ while (offset < segment.length) {
6385
+ const scanned = scanShellWord(segment, offset);
6386
+ if (!scanned) break;
6387
+ words.push(scanned.word);
6388
+ if (scanned.end <= offset) break;
6389
+ offset = scanned.end;
6390
+ }
6391
+ return words;
6392
+ }
6393
+ function commandExecutable(words) {
6394
+ const normalized = unwrapPackageManagerExecutable(words);
6395
+ const executable = normalized[0];
6396
+ return executable ? basename(executable).replace(/\.cmd$/i, "").toLowerCase() : void 0;
6397
+ }
6398
+ function unwrapPackageManagerExecutable(words) {
6399
+ if (words.length === 0) return [];
6400
+ const first = basename(words[0] ?? "").toLowerCase();
6401
+ if (first === "pnpm" || first === "yarn") {
6402
+ const second = words[1]?.toLowerCase();
6403
+ return words.slice(second === "exec" || second === "dlx" ? 2 : 1);
6404
+ }
6405
+ if (first === "npx") return words.slice(1).filter((word) => !/^--(?:yes|no-install)$/u.test(word));
6406
+ if (first === "npm" && words[1]?.toLowerCase() === "exec") {
6407
+ let offset = 2;
6408
+ while (/^--(?:yes|no-install)$/u.test(words[offset] ?? "")) offset += 1;
6409
+ if (words[offset] === "--") offset += 1;
6410
+ return words.slice(offset);
6411
+ }
6412
+ return [...words];
6413
+ }
6414
+ function coverageFlagCommandState(words) {
6415
+ const command = unwrapPackageManagerExecutable(words);
6416
+ const executable = basename(command[0] ?? "").replace(/\.cmd$/i, "").toLowerCase();
6417
+ const args = command.slice(1);
6418
+ if (!executable) return "none";
6419
+ if (executable === "node") {
6420
+ if (!args.includes("--test")) return "none";
6421
+ return flagState(args, ["--experimental-test-coverage"], ["--no-experimental-test-coverage"]);
6422
+ }
6423
+ if (executable === "pytest" || executable === "py.test") {
6424
+ return flagState(args, ["--cov"], ["--no-cov"]);
6425
+ }
6426
+ if (executable === "bun" || executable === "deno") {
6427
+ if (args[0]?.toLowerCase() !== "test") return "none";
6428
+ return flagState(args.slice(1), ["--coverage"], ["--no-coverage"]);
6429
+ }
6430
+ if (executable === "jest") {
6431
+ return flagState(
6432
+ args,
6433
+ ["--coverage", "--collectcoverage", "--collect-coverage"],
6434
+ ["--no-coverage", "--no-collectcoverage", "--no-collect-coverage"]
6435
+ );
6436
+ }
6437
+ if (executable === "vitest" || executable === "bnt") {
6438
+ return flagState(args, ["--coverage"], ["--no-coverage"]);
6439
+ }
6440
+ return "none";
6441
+ }
6442
+ function isRecognizedCoverageFlagRunner(words) {
6443
+ const command = unwrapPackageManagerExecutable(words);
6444
+ const executable = basename(command[0] ?? "").replace(/\.cmd$/i, "").toLowerCase();
6445
+ if (["bnt", "vitest", "jest", "pytest", "py.test"].includes(executable)) return true;
6446
+ if (executable === "node") return command.slice(1).includes("--test");
6447
+ return (executable === "bun" || executable === "deno") && command[1]?.toLowerCase() === "test";
6448
+ }
6449
+ function flagState(args, enablingNames, disablingNames) {
6450
+ let enabled = false;
6451
+ let disabled = false;
6452
+ let unresolved = false;
6453
+ const enabling = new Set(enablingNames.map((name) => name.toLowerCase()));
6454
+ const disabling = new Set(disablingNames.map((name) => name.toLowerCase()));
6455
+ for (let index = 0; index < args.length; index += 1) {
6456
+ const raw = args[index] ?? "";
6457
+ const lower = raw.toLowerCase();
6458
+ if (disabling.has(lower)) {
6459
+ disabled = true;
6460
+ continue;
6461
+ }
6462
+ const [name, assigned] = lower.split("=", 2);
6463
+ if (!name || !enabling.has(name)) continue;
6464
+ if (assigned !== void 0) {
6465
+ if (assigned === "false" || assigned === "0" || assigned === "off") disabled = true;
6466
+ else if (/\$|\{|\}/u.test(assigned)) unresolved = true;
6467
+ else enabled = true;
6468
+ continue;
6469
+ }
6470
+ const next = args[index + 1]?.toLowerCase();
6471
+ if (next === "false" || next === "0" || next === "off") {
6472
+ disabled = true;
6473
+ index += 1;
6474
+ } else if (next && /\$|\{|\}/u.test(next)) {
6475
+ unresolved = true;
6476
+ index += 1;
6477
+ } else {
6478
+ enabled = true;
6479
+ }
6480
+ }
6481
+ if (unresolved || enabled && disabled) return "unresolved";
6482
+ return enabled && !disabled ? "collects" : "none";
6483
+ }
6484
+ function containsPotentialCoverageFlag(command) {
6485
+ return /(?:^|\s)--(?:experimental-test-coverage|collect-?coverage|no-coverage|coverage|no-cov|cov)(?:[=\s]|$)/iu.test(
6486
+ command
6487
+ );
6488
+ }
6489
+ function reachableShellWrapper(words, repoFiles) {
6490
+ const executable = basename(words[0] ?? "").toLowerCase();
6491
+ const candidate = /^(?:ba|da|k|z)?sh$/u.test(executable) ? words[1] : words[0];
6492
+ if (!candidate || !/(?:^|\/)\.?[^\s]*\.sh$/i.test(candidate)) return void 0;
6493
+ const normalized = candidate.replace(/^\.\//u, "");
6494
+ if (normalized.startsWith("/") || normalized.split("/").includes("..")) return void 0;
6495
+ return repoFiles.has(normalized) ? normalized : void 0;
6496
+ }
5483
6497
  function commandInvokesCoverageTool(command) {
5484
6498
  return normalizedCommandInvokesCoverageTool(normalizeCoverageCommandPrefixes(command), 0);
5485
6499
  }
@@ -6094,7 +7108,69 @@ function fileContains(repoPath, file, pattern) {
6094
7108
  if (!isRegularFile(fullPath)) return false;
6095
7109
  return pattern.test(readRepoText(fullPath, "utf8"));
6096
7110
  }
6097
- 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) {
6098
7174
  const fullPath = join(repoPath, file);
6099
7175
  if (!isRegularFile(fullPath)) return null;
6100
7176
  const contents = readRepoText(fullPath, "utf8");
@@ -6110,6 +7186,10 @@ function findCommittedSecretInFile(repoPath, file, useV36Detectors = false, useV
6110
7186
  allowCredentialNamedDigest: useV36Detectors
6111
7187
  });
6112
7188
  if (secret) return secret;
7189
+ if (useV23PemPrivateKeyGrammar) {
7190
+ const pemSecret = findPemPrivateKeyAssignment(secretScanContents, file);
7191
+ if (pemSecret) return pemSecret;
7192
+ }
6113
7193
  if (useV39Detectors && isEnvHistoryPath(file, useV36Detectors)) {
6114
7194
  const weakCredential = findWeakExplicitEnvCredential(secretScanContents);
6115
7195
  if (weakCredential) return weakCredential;
@@ -6202,7 +7282,7 @@ function hasTemplateOnlyEnvContent(repoPath, file) {
6202
7282
  return value.length === 0 || isPlaceholderSecretValue(value);
6203
7283
  });
6204
7284
  }
6205
- function findSecretFingerprintsInFile(repoPath, file, useV36Detectors = false, useV39Detectors = false, useV47Detectors = false) {
7285
+ function findSecretFingerprintsInFile(repoPath, file, useV36Detectors = false, useV39Detectors = false, useV47Detectors = false, useV23PemPrivateKeyGrammar = false) {
6206
7286
  const fullPath = join(repoPath, file);
6207
7287
  if (!isRegularFile(fullPath)) return [];
6208
7288
  const contents = readRepoText(fullPath, "utf8");
@@ -6219,6 +7299,10 @@ function findSecretFingerprintsInFile(repoPath, file, useV36Detectors = false, u
6219
7299
  Number.POSITIVE_INFINITY,
6220
7300
  { allowCredentialNamedDigest: useV36Detectors }
6221
7301
  ).map(({ valueFingerprint }) => valueFingerprint);
7302
+ if (useV23PemPrivateKeyGrammar) {
7303
+ const pemSecret = findPemPrivateKeyAssignment(scanContents, file);
7304
+ if (pemSecret) fingerprints.push(pemSecret.valueFingerprint);
7305
+ }
6222
7306
  if (useV39Detectors && isEnvHistoryPath(file, useV36Detectors)) {
6223
7307
  const weakCredential = findWeakExplicitEnvCredential(scanContents);
6224
7308
  if (weakCredential) fingerprints.push(weakCredential.valueFingerprint);
@@ -6362,24 +7446,32 @@ function declareHistoryFailure(scanLimitations, reason, operation, maxBufferByte
6362
7446
  `${describeGitFailure(reason, operation, maxBufferBytes)} Cejel continued with reduced Git history evidence.`
6363
7447
  );
6364
7448
  }
6365
- 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) {
6366
7450
  const historyEntries = readCredentialHistoryEntries(
6367
7451
  repoPath,
6368
7452
  scanLimitations,
6369
- useV36Detectors
7453
+ useV36Detectors,
7454
+ useV23PemPrivateKeyGrammar
6370
7455
  );
6371
7456
  const head = excludeHead ? readGitHead(repoPath) : null;
6372
7457
  let scannedFiles = 0;
7458
+ let v23WidenedOnlyScanned = 0;
6373
7459
  let envPathEvidence = null;
6374
7460
  for (const entry of historyEntries) {
6375
7461
  if (head && entry.commit === head) continue;
6376
7462
  if (authoredProductionOnly && !(useV47Detectors ? isV47AuthoredProductionPath(entry.path) : useV39Detectors ? isV39AuthoredProductionPath(entry.path) : isAuthoredProductionPath(entry.path))) {
6377
7463
  continue;
6378
7464
  }
6379
- if (scannedFiles >= HISTORY_SECRET_SCAN_CREDENTIAL_BLOB_LIMIT) {
6380
- 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;
6381
7474
  }
6382
- scannedFiles += 1;
6383
7475
  const contents = readGitBlob(repoPath, entry.commit, entry.path, scanLimitations);
6384
7476
  if (!contents) continue;
6385
7477
  const scanContents = prepareCredentialScanContents(
@@ -6393,6 +7485,12 @@ function collectHistorySecretEvidence(repoPath, excludeHead, currentSecretFinger
6393
7485
  let secretMatch = findRealSecretAssignment(scanContents, currentFingerprints, {
6394
7486
  allowCredentialNamedDigest: useV36Detectors
6395
7487
  });
7488
+ if (!secretMatch && useV23PemPrivateKeyGrammar) {
7489
+ const pemMatch = findPemPrivateKeyAssignment(scanContents, entry.path);
7490
+ if (pemMatch && !currentFingerprints.has(pemMatch.valueFingerprint)) {
7491
+ secretMatch = pemMatch;
7492
+ }
7493
+ }
6396
7494
  if (!secretMatch && useV39Detectors && isEnvHistoryPath(entry.path, useV36Detectors, useV47Detectors)) {
6397
7495
  const weakCredential = findWeakExplicitEnvCredential(scanContents);
6398
7496
  if (weakCredential && !currentFingerprints.has(weakCredential.valueFingerprint)) {
@@ -6403,11 +7501,15 @@ function collectHistorySecretEvidence(repoPath, excludeHead, currentSecretFinger
6403
7501
  return {
6404
7502
  evidence: {
6405
7503
  kind: "secret_scan",
6406
- 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
+ ),
6407
7508
  path: entry.path,
6408
7509
  line: secretMatch.line,
6409
7510
  contentHash: entry.commit
6410
7511
  },
7512
+ secretKind: secretMatch.kind ?? null,
6411
7513
  envPathEvidence,
6412
7514
  truncated: false
6413
7515
  };
@@ -6421,9 +7523,9 @@ function collectHistorySecretEvidence(repoPath, excludeHead, currentSecretFinger
6421
7523
  };
6422
7524
  }
6423
7525
  }
6424
- return { evidence: null, envPathEvidence, truncated: false };
7526
+ return { evidence: null, secretKind: null, envPathEvidence, truncated: false };
6425
7527
  }
6426
- function readCredentialHistoryEntries(repoPath, scanLimitations, useV36Detectors = false) {
7528
+ function readCredentialHistoryEntries(repoPath, scanLimitations, useV36Detectors = false, useV23PemPrivateKeyGrammar = false) {
6427
7529
  const entries = [];
6428
7530
  const seen = /* @__PURE__ */ new Set();
6429
7531
  const appendEntry = (entry) => {
@@ -6468,7 +7570,7 @@ function readCredentialHistoryEntries(repoPath, scanLimitations, useV36Detectors
6468
7570
  scanLimitations.add(CONTROL_CHARACTER_HISTORY_PATH_LIMITATION);
6469
7571
  continue;
6470
7572
  }
6471
- if (!isCredentialHistoryPath(path, useV36Detectors)) continue;
7573
+ if (!isCredentialHistoryPath(path, useV36Detectors, useV23PemPrivateKeyGrammar)) continue;
6472
7574
  appendEntry({ commit: currentCommit, path });
6473
7575
  }
6474
7576
  }
@@ -6476,17 +7578,19 @@ function readCredentialHistoryEntries(repoPath, scanLimitations, useV36Detectors
6476
7578
  for (const entry of readDeletedCredentialHistoryEntries(
6477
7579
  repoPath,
6478
7580
  scanLimitations,
6479
- useV36Detectors
7581
+ useV36Detectors,
7582
+ useV23PemPrivateKeyGrammar
6480
7583
  )) {
6481
7584
  appendEntry(entry);
6482
7585
  }
6483
7586
  return entries;
6484
7587
  }
6485
- function readDeletedCredentialHistoryEntries(repoPath, scanLimitations, useV36Detectors = false) {
7588
+ function readDeletedCredentialHistoryEntries(repoPath, scanLimitations, useV36Detectors = false, useV23PemPrivateKeyGrammar = false) {
6486
7589
  const deletedPaths = readDeletedCredentialHistoryPaths(
6487
7590
  repoPath,
6488
7591
  scanLimitations,
6489
- useV36Detectors
7592
+ useV36Detectors,
7593
+ useV23PemPrivateKeyGrammar
6490
7594
  );
6491
7595
  const entries = [];
6492
7596
  const seen = /* @__PURE__ */ new Set();
@@ -6500,7 +7604,7 @@ function readDeletedCredentialHistoryEntries(repoPath, scanLimitations, useV36De
6500
7604
  }
6501
7605
  return entries;
6502
7606
  }
6503
- function readDeletedCredentialHistoryPaths(repoPath, scanLimitations, useV36Detectors = false) {
7607
+ function readDeletedCredentialHistoryPaths(repoPath, scanLimitations, useV36Detectors = false, useV23PemPrivateKeyGrammar = false) {
6504
7608
  const historyResult = execGit(
6505
7609
  [
6506
7610
  "log",
@@ -6528,7 +7632,7 @@ function readDeletedCredentialHistoryPaths(repoPath, scanLimitations, useV36Dete
6528
7632
  continue;
6529
7633
  }
6530
7634
  if (!path || seen.has(path)) continue;
6531
- if (!isCredentialHistoryPath(path, useV36Detectors)) continue;
7635
+ if (!isCredentialHistoryPath(path, useV36Detectors, useV23PemPrivateKeyGrammar)) continue;
6532
7636
  seen.add(path);
6533
7637
  paths.push(path);
6534
7638
  }
@@ -6546,14 +7650,24 @@ function readHistoryCommitsForPath(repoPath, path, scanLimitations) {
6546
7650
  }
6547
7651
  return historyResult.stdout.split("\n").map((line) => line.trim()).filter((line) => /^[0-9a-f]{40}$/i.test(line));
6548
7652
  }
6549
- 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) {
6550
7661
  if (isHardExcludedPath(path)) return false;
6551
7662
  const lowerPath = path.toLowerCase();
6552
- const basename3 = lowerPath.split("/").at(-1) ?? lowerPath;
7663
+ const basename3 = historyPathBasename(lowerPath);
6553
7664
  if (/\.env(?:\.|$)/.test(basename3)) {
6554
7665
  return true;
6555
7666
  }
6556
- 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);
6557
7671
  }
6558
7672
  function isEnvHistoryPath(path, useV36Detectors = false, useV47Detectors = false) {
6559
7673
  if (isEnvTemplatePath(path, useV47Detectors)) return false;
@@ -7325,14 +8439,14 @@ var WITAN_RUBRIC = [
7325
8439
 
7326
8440
  // src/witan/scoring.ts
7327
8441
  function usesV17DetectorClosure2(rubricVersion) {
7328
- 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;
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;
7329
8443
  }
7330
8444
  var MAX_SIGNAL_ADJUSTMENT = 0.8;
7331
- function createWitanReport(input, inputSignals, rubric = WITAN_RUBRIC) {
8445
+ function createWitanReport(input, inputSignals, rubric = WITAN_RUBRIC, toolVersion) {
7332
8446
  const parsedInput = WitanReportInputSchema.parse(input);
7333
8447
  const signalsByCriterion = mergeSignalsByCriterion(parsedInput.signals);
7334
8448
  const readFailureAbstentions = new Set(
7335
- [...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)
7336
8450
  );
7337
8451
  const hasInputSignals = inputSignals != null && inputSignals.length > 0;
7338
8452
  const inputSignalsByDimension = /* @__PURE__ */ new Map();
@@ -7437,7 +8551,8 @@ function createWitanReport(input, inputSignals, rubric = WITAN_RUBRIC) {
7437
8551
  // alter it (see classifyRepoArchetype in repo-signals.ts); per-criterion scoring above is
7438
8552
  // unaffected by archetype, only presentation layers (badge/terminal/verdict) key off it.
7439
8553
  ...parsedInput.archetype ? { archetype: parsedInput.archetype } : {},
7440
- ...insufficientSourceReason ? { insufficientSourceReason } : {}
8554
+ ...insufficientSourceReason ? { insufficientSourceReason } : {},
8555
+ ...toolVersion ? { toolVersion } : {}
7441
8556
  });
7442
8557
  }
7443
8558
  function applySignalAdjustment(nativeScore, dimension, signals) {
@@ -7484,7 +8599,7 @@ function statusForScore(score) {
7484
8599
  }
7485
8600
  function statusAfterInputAdjustment(nativeStatus, nativeScore, adjustment, rubricVersion) {
7486
8601
  if (adjustment === 0) return nativeStatus;
7487
- if (rubricVersion !== WITAN_RUBRIC_VERSION_V8 && rubricVersion !== WITAN_RUBRIC_VERSION_V9 && rubricVersion !== WITAN_RUBRIC_VERSION_V10 && rubricVersion !== WITAN_RUBRIC_VERSION_V11 && rubricVersion !== WITAN_RUBRIC_VERSION_V12 && rubricVersion !== WITAN_RUBRIC_VERSION_V13 && rubricVersion !== WITAN_RUBRIC_VERSION_V14 && rubricVersion !== WITAN_RUBRIC_VERSION_V15 && rubricVersion !== WITAN_RUBRIC_VERSION_V16 && 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) {
8602
+ if (rubricVersion !== WITAN_RUBRIC_VERSION_V8 && rubricVersion !== WITAN_RUBRIC_VERSION_V9 && rubricVersion !== WITAN_RUBRIC_VERSION_V10 && rubricVersion !== WITAN_RUBRIC_VERSION_V11 && rubricVersion !== WITAN_RUBRIC_VERSION_V12 && rubricVersion !== WITAN_RUBRIC_VERSION_V13 && rubricVersion !== WITAN_RUBRIC_VERSION_V14 && rubricVersion !== WITAN_RUBRIC_VERSION_V15 && rubricVersion !== WITAN_RUBRIC_VERSION_V16 && 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) {
7488
8603
  return statusForScore(roundScore(Math.max(0, nativeScore - adjustment)));
7489
8604
  }
7490
8605
  const statusFloor = {
@@ -7507,6 +8622,14 @@ function statusAfterInputAdjustment(nativeStatus, nativeScore, adjustment, rubri
7507
8622
  );
7508
8623
  return (severity[adjustedSemanticStatus] ?? nativeSeverity) > nativeSeverity ? adjustedSemanticStatus : nativeStatus;
7509
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
+ }
7510
8633
  function mergeSignalsByCriterion(signals) {
7511
8634
  const merged = /* @__PURE__ */ new Map();
7512
8635
  for (const signal of signals) {
@@ -7515,21 +8638,25 @@ function mergeSignalsByCriterion(signals) {
7515
8638
  merged.set(signal.criterionId, signal);
7516
8639
  continue;
7517
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;
7518
8644
  merged.set(signal.criterionId, {
7519
8645
  criterionId: signal.criterionId,
7520
8646
  positiveEvidence: [...existing.positiveEvidence, ...signal.positiveEvidence],
7521
8647
  findings: [...existing.findings, ...signal.findings],
7522
8648
  metrics: [...existing.metrics ?? [], ...signal.metrics ?? []],
7523
8649
  notes: signal.notes ?? existing.notes,
7524
- // Read-failure abstention wins on merge: partial sibling evidence cannot turn an
7525
- // incomplete criterion into a score. Otherwise N/A retains its historical precedence.
7526
- ...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 } : {}
7527
8654
  });
7528
8655
  }
7529
8656
  return merged;
7530
8657
  }
7531
8658
  function usesMetricScoring(rubricVersion) {
7532
- return rubricVersion === WITAN_RUBRIC_VERSION_V1 || rubricVersion === WITAN_RUBRIC_VERSION_V2 || rubricVersion === WITAN_RUBRIC_VERSION_V3 || rubricVersion === WITAN_RUBRIC_VERSION_V4 || rubricVersion === WITAN_RUBRIC_VERSION_V5 || rubricVersion === WITAN_RUBRIC_VERSION_V6 || rubricVersion === WITAN_RUBRIC_VERSION_V7 || rubricVersion === WITAN_RUBRIC_VERSION_V8 || rubricVersion === WITAN_RUBRIC_VERSION_V9 || rubricVersion === WITAN_RUBRIC_VERSION_V10 || rubricVersion === WITAN_RUBRIC_VERSION_V11 || rubricVersion === WITAN_RUBRIC_VERSION_V12 || rubricVersion === WITAN_RUBRIC_VERSION_V13 || rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15 || rubricVersion === WITAN_RUBRIC_VERSION_V16 || 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_TRADING_RUBRIC_VERSION_V0;
8659
+ return rubricVersion === WITAN_RUBRIC_VERSION_V1 || rubricVersion === WITAN_RUBRIC_VERSION_V2 || rubricVersion === WITAN_RUBRIC_VERSION_V3 || rubricVersion === WITAN_RUBRIC_VERSION_V4 || rubricVersion === WITAN_RUBRIC_VERSION_V5 || rubricVersion === WITAN_RUBRIC_VERSION_V6 || rubricVersion === WITAN_RUBRIC_VERSION_V7 || rubricVersion === WITAN_RUBRIC_VERSION_V8 || rubricVersion === WITAN_RUBRIC_VERSION_V9 || rubricVersion === WITAN_RUBRIC_VERSION_V10 || rubricVersion === WITAN_RUBRIC_VERSION_V11 || rubricVersion === WITAN_RUBRIC_VERSION_V12 || rubricVersion === WITAN_RUBRIC_VERSION_V13 || rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15 || rubricVersion === WITAN_RUBRIC_VERSION_V16 || 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 || rubricVersion === WITAN_TRADING_RUBRIC_VERSION_V0;
7533
8660
  }
7534
8661
  function unmeasuredStatus(rubricVersion) {
7535
8662
  return rubricVersion === WITAN_TRADING_RUBRIC_VERSION_V0 ? "unverified" : "insufficient_data";
@@ -7909,7 +9036,7 @@ function calibratedCriterionStateV11(criterionId, metrics) {
7909
9036
  return void 0;
7910
9037
  }
7911
9038
  function ensureFindingsExplainStatus(criterionId, score, status, findings, evidence, rubricVersion) {
7912
- if (rubricVersion !== WITAN_RUBRIC_VERSION_V8 && rubricVersion !== WITAN_RUBRIC_VERSION_V9 && rubricVersion !== WITAN_RUBRIC_VERSION_V10 && rubricVersion !== WITAN_RUBRIC_VERSION_V11 && rubricVersion !== WITAN_RUBRIC_VERSION_V12 && rubricVersion !== WITAN_RUBRIC_VERSION_V13 && rubricVersion !== WITAN_RUBRIC_VERSION_V14 && rubricVersion !== WITAN_RUBRIC_VERSION_V15 && rubricVersion !== WITAN_RUBRIC_VERSION_V16 && 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 && findings.length > 0) {
9039
+ if (rubricVersion !== WITAN_RUBRIC_VERSION_V8 && rubricVersion !== WITAN_RUBRIC_VERSION_V9 && rubricVersion !== WITAN_RUBRIC_VERSION_V10 && rubricVersion !== WITAN_RUBRIC_VERSION_V11 && rubricVersion !== WITAN_RUBRIC_VERSION_V12 && rubricVersion !== WITAN_RUBRIC_VERSION_V13 && rubricVersion !== WITAN_RUBRIC_VERSION_V14 && rubricVersion !== WITAN_RUBRIC_VERSION_V15 && rubricVersion !== WITAN_RUBRIC_VERSION_V16 && 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 && findings.length > 0) {
7913
9040
  return [...findings];
7914
9041
  }
7915
9042
  if (status !== "critical" && status !== "warning") return [...findings];
@@ -7959,7 +9086,12 @@ function scoreRepoWithPublicCejel(options) {
7959
9086
  ...options.rubricVersion ? { rubricVersion: options.rubricVersion } : {}
7960
9087
  });
7961
9088
  const inputSignals = resolvePublicIngestSignals(options);
7962
- 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
+ );
7963
9095
  if (report.verdict !== "insufficient_source" || !isWitanNoMeasurementAbstention(report)) {
7964
9096
  return report;
7965
9097
  }
@@ -8217,7 +9349,8 @@ function runCejelScan(options) {
8217
9349
  // the repository. Direct public/leaderboard scans default this off because their targets
8218
9350
  // are third-party repositories and must opt in explicitly.
8219
9351
  autoDiscoverIngest: true,
8220
- warnOnEmptyIngestMatch: options.warnOnEmptyIngestMatch
9352
+ warnOnEmptyIngestMatch: options.warnOnEmptyIngestMatch,
9353
+ ...options.toolVersion ? { toolVersion: options.toolVersion } : {}
8221
9354
  });
8222
9355
  const summary = buildWitanCliSummary(report);
8223
9356
  return { report, summary, generatedAt };
@@ -8229,6 +9362,7 @@ export {
8229
9362
  CALLER_CONTEXT_PRODUCT_IDENTITY_NOTICE,
8230
9363
  CERTIFICATE_GLOSSARY,
8231
9364
  formatCertificateMetricLabel,
9365
+ formatCertificateMetricAppliedWeightPercent,
8232
9366
  formatCertificateMetricValue,
8233
9367
  buildRelyingPartySummary,
8234
9368
  WITAN_RUBRIC_VERSION_V22,