@cejel/cejel 0.2.2 → 0.3.2

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.
@@ -1,33 +1,25 @@
1
1
  #!/usr/bin/env node
2
- var __create = Object.create;
3
- var __defProp = Object.defineProperty;
4
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
- var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __commonJS = (cb, mod) => function __require() {
9
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
10
- };
11
- var __export = (target, all) => {
12
- for (var name in all)
13
- __defProp(target, name, { get: all[name], enumerable: true });
14
- };
15
- var __copyProps = (to, from, except, desc) => {
16
- if (from && typeof from === "object" || typeof from === "function") {
17
- for (let key of __getOwnPropNames(from))
18
- if (!__hasOwnProp.call(to, key) && key !== except)
19
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
20
- }
21
- return to;
22
- };
23
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
24
- // If the importer is in node compatibility mode or this is not an ESM
25
- // file that has been converted to a CommonJS file using a Babel-
26
- // compatible transform (i.e. "__esModule" has not been set), then set
27
- // "default" to the CommonJS "module.exports" for node compatibility.
28
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
29
- mod
30
- ));
2
+ import { createRequire as __cejelCreateRequire } from 'node:module';
3
+ const require = __cejelCreateRequire(import.meta.url);
4
+ import {
5
+ __export,
6
+ describeGitFailure,
7
+ execGit,
8
+ init_esm_shims,
9
+ isExpectedGitAbsence,
10
+ readRepoText,
11
+ readRepoTextPrefix,
12
+ recordContentSkip,
13
+ recordFilesystemSkip,
14
+ trackContentReads,
15
+ withContentReadCriterion
16
+ } from "./chunk-LZVKQV3R.js";
17
+
18
+ // src/witan/schemas.ts
19
+ init_esm_shims();
20
+
21
+ // node_modules/.pnpm/zod@3.25.76/node_modules/zod/index.js
22
+ init_esm_shims();
31
23
 
32
24
  // node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
33
25
  var external_exports = {};
@@ -140,8 +132,19 @@ __export(external_exports, {
140
132
  util: () => util,
141
133
  void: () => voidType
142
134
  });
135
+ init_esm_shims();
136
+
137
+ // node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/errors.js
138
+ init_esm_shims();
139
+
140
+ // node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/locales/en.js
141
+ init_esm_shims();
142
+
143
+ // node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/ZodError.js
144
+ init_esm_shims();
143
145
 
144
146
  // node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/util.js
147
+ init_esm_shims();
145
148
  var util;
146
149
  (function(util2) {
147
150
  util2.assertEqual = (_) => {
@@ -506,6 +509,7 @@ function getErrorMap() {
506
509
  }
507
510
 
508
511
  // node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
512
+ init_esm_shims();
509
513
  var makeIssue = (params) => {
510
514
  const { data, path, errorMaps, issueData } = params;
511
515
  const fullPath = [...path, ...issueData.path || []];
@@ -615,7 +619,11 @@ var isDirty = (x) => x.status === "dirty";
615
619
  var isValid = (x) => x.status === "valid";
616
620
  var isAsync = (x) => typeof Promise !== "undefined" && x instanceof Promise;
617
621
 
622
+ // node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.js
623
+ init_esm_shims();
624
+
618
625
  // node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/errorUtil.js
626
+ init_esm_shims();
619
627
  var errorUtil;
620
628
  (function(errorUtil2) {
621
629
  errorUtil2.errToObj = (message) => typeof message === "string" ? { message } : message || {};
@@ -4188,8 +4196,45 @@ var WitanCriterionSignalSchema = external_exports.object({
4188
4196
  findings: external_exports.array(WitanFindingSchema).default([]),
4189
4197
  metrics: external_exports.array(WitanCriterionMetricSchema).default([]),
4190
4198
  notes: external_exports.string().min(1).max(1e3).optional(),
4191
- notApplicable: external_exports.literal(true).optional()
4192
- }).strict();
4199
+ notApplicable: external_exports.literal(true).optional(),
4200
+ // A collector reached relevant files but could not read their contents. This is a
4201
+ // measurement gap, never negative evidence: scoreCriterion maps it to insufficient_data.
4202
+ insufficientData: external_exports.literal(true).optional()
4203
+ }).strict().refine((signal) => !(signal.notApplicable && signal.insufficientData), {
4204
+ message: "A criterion cannot be both not applicable and insufficient data."
4205
+ });
4206
+ var WitanContentReadSummarySchema = external_exports.object({
4207
+ skipped: external_exports.number().int().nonnegative(),
4208
+ byReason: external_exports.object({
4209
+ unreadable: external_exports.number().int().nonnegative(),
4210
+ tooLarge: external_exports.number().int().nonnegative(),
4211
+ excludedByExtension: external_exports.number().int().nonnegative(),
4212
+ deniedPath: external_exports.number().int().nonnegative(),
4213
+ nonRegularFile: external_exports.number().int().nonnegative()
4214
+ }).strict(),
4215
+ unreadableByErrno: external_exports.record(external_exports.string().min(1).max(32), external_exports.number().int().positive()),
4216
+ affectedCriteria: external_exports.array(WitanCriterionIdSchema)
4217
+ }).strict().superRefine((summary, context) => {
4218
+ const reasonTotal = Object.values(summary.byReason).reduce((sum, count) => sum + count, 0);
4219
+ if (reasonTotal !== summary.skipped) {
4220
+ context.addIssue({
4221
+ code: external_exports.ZodIssueCode.custom,
4222
+ message: "Content-read reason counts must sum to skipped.",
4223
+ path: ["byReason"]
4224
+ });
4225
+ }
4226
+ const errnoTotal = Object.values(summary.unreadableByErrno).reduce(
4227
+ (sum, count) => sum + count,
4228
+ 0
4229
+ );
4230
+ if (errnoTotal !== summary.byReason.unreadable) {
4231
+ context.addIssue({
4232
+ code: external_exports.ZodIssueCode.custom,
4233
+ message: "Unreadable errno counts must sum to the unreadable count.",
4234
+ path: ["unreadableByErrno"]
4235
+ });
4236
+ }
4237
+ });
4193
4238
  var WitanRepoRefSchema = external_exports.object({
4194
4239
  path: external_exports.string().min(1).max(700).optional(),
4195
4240
  url: external_exports.string().url().optional(),
@@ -4205,7 +4250,9 @@ var WitanReportInputSchema = external_exports.object({
4205
4250
  rubricVersion: external_exports.string().min(1).max(120).default(WITAN_RUBRIC_VERSION),
4206
4251
  signals: external_exports.array(WitanCriterionSignalSchema).default([]),
4207
4252
  archetype: WitanRepoArchetypeSchema.optional(),
4208
- insufficientSourceReason: external_exports.string().min(1).max(2e3).optional()
4253
+ insufficientSourceReason: external_exports.string().min(1).max(2e3).optional(),
4254
+ scanLimitations: external_exports.array(external_exports.string().min(1).max(1e3)).max(16).default([]),
4255
+ contentReadSummary: WitanContentReadSummarySchema.optional()
4209
4256
  }).strict();
4210
4257
  var WitanCriterionScoreSchema = external_exports.object({
4211
4258
  id: WitanCriterionIdSchema,
@@ -4225,14 +4272,21 @@ var WitanInputSignalFindingSchema = external_exports.object({
4225
4272
  message: external_exports.string().min(1).max(500),
4226
4273
  location: external_exports.string().max(700).optional()
4227
4274
  }).strict();
4275
+ var WitanIngestProvenanceSchema = external_exports.enum(["operator_supplied", "auto_discovered"]);
4228
4276
  var WitanInputSignalSchema = external_exports.object({
4229
4277
  source: external_exports.string().min(1).max(120),
4278
+ // Adapters can still be used directly by internal callers; absent provenance retains the
4279
+ // historical operator-supplied meaning. The ingest funnel always sets this explicitly.
4280
+ provenance: WitanIngestProvenanceSchema.optional(),
4230
4281
  dimension: WitanCriterionIdSchema,
4231
4282
  findings: external_exports.array(WitanInputSignalFindingSchema),
4232
4283
  weight: external_exports.number().min(0).max(1)
4233
4284
  }).strict();
4234
4285
  var WitanConsumedSignalSummarySchema = external_exports.object({
4235
4286
  source: external_exports.string().min(1).max(120),
4287
+ // Optional on read for compatibility with already-published reports. Newly generated
4288
+ // reports always carry the explicit value copied from WitanInputSignal.
4289
+ provenance: WitanIngestProvenanceSchema.optional(),
4236
4290
  dimension: WitanCriterionIdSchema,
4237
4291
  findingCount: external_exports.number().int().min(0),
4238
4292
  severityBreakdown: external_exports.object({
@@ -4269,12 +4323,13 @@ var WitanReportCommonSchema = external_exports.object({
4269
4323
  productSlug: external_exports.string().regex(/^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$/),
4270
4324
  productDisplayName: external_exports.string().min(1).max(120),
4271
4325
  repo: WitanRepoRefSchema,
4272
- generatedAt: external_exports.string().datetime({ offset: true }),
4273
4326
  rubricVersion: external_exports.string().min(1).max(120),
4274
4327
  // Rubric-driven: length matches whatever rubric produced this report, not a fixed enum.
4275
4328
  criteria: external_exports.array(WitanCriterionScoreSchema).min(1),
4276
4329
  consumedSignals: external_exports.array(WitanConsumedSignalSummarySchema).optional(),
4277
- archetype: WitanRepoArchetypeSchema.optional()
4330
+ archetype: WitanRepoArchetypeSchema.optional(),
4331
+ scanLimitations: external_exports.array(external_exports.string().min(1).max(1e3)).max(16).optional(),
4332
+ contentReadSummary: WitanContentReadSummarySchema.optional()
4278
4333
  });
4279
4334
  var WitanScoredReportSchema = WitanReportCommonSchema.extend({
4280
4335
  verdict: WitanScoredVerdictSchema,
@@ -4354,6 +4409,12 @@ var WitanAttestationStatementSchema = external_exports.object({
4354
4409
  artifact: external_exports.literal("report.json"),
4355
4410
  sha256: external_exports.string().regex(/^[a-f0-9]{64}$/)
4356
4411
  }).strict(),
4412
+ externalSignalProvenance: external_exports.array(
4413
+ external_exports.object({
4414
+ source: external_exports.string().min(1).max(120),
4415
+ provenance: WitanIngestProvenanceSchema
4416
+ }).strict()
4417
+ ).default([]),
4357
4418
  outcome: WitanAttestationOutcomeSchema,
4358
4419
  assurance: external_exports.object({
4359
4420
  status: external_exports.literal("unsigned"),
@@ -4364,7 +4425,11 @@ var WitanAttestationStatementSchema = external_exports.object({
4364
4425
  }).strict()
4365
4426
  }).strict();
4366
4427
 
4428
+ // src/witan/html.ts
4429
+ init_esm_shims();
4430
+
4367
4431
  // src/witan/abstention.ts
4432
+ init_esm_shims();
4368
4433
  var WITAN_NO_MEASUREMENT_REASON = "No free-core rubric criterion produced a measurable signal. Cejel abstains rather than publish a numeric zero for an entirely unmeasured repository.";
4369
4434
  var WITAN_AUTHENTICATED_A1_ABSENCE_SUMMARY = "Reviewable source is present, but the complete tracked inventory contains no concrete test file, configured test runner, CI test command, or coverage configuration.";
4370
4435
  function isWitanNoMeasurementAbstention(report) {
@@ -4377,6 +4442,7 @@ function renderWitanAbstentionLabel(report) {
4377
4442
  }
4378
4443
 
4379
4444
  // src/witan/coverage.ts
4445
+ init_esm_shims();
4380
4446
  var LOW_CONFIDENCE_COVERAGE_THRESHOLD = 0.5;
4381
4447
  function isMeasuredCriterionStatus(status) {
4382
4448
  return status !== "not_applicable" && status !== "insufficient_data";
@@ -4410,6 +4476,7 @@ function formatCoverageCounts(coverage) {
4410
4476
  }
4411
4477
 
4412
4478
  // src/witan/external-findings.ts
4479
+ init_esm_shims();
4413
4480
  var EXTERNAL_FINDINGS_DISPLAY_LIMIT = 10;
4414
4481
  var SEVERITY_RANK = {
4415
4482
  critical: 0,
@@ -4463,6 +4530,7 @@ function collectExternalFindings(consumedSignals) {
4463
4530
  }
4464
4531
 
4465
4532
  // src/witan/finding-presentation.ts
4533
+ init_esm_shims();
4466
4534
  var METRIC_DERIVED_FINDING_PATTERN = /^[A-Z]\d+ metric-derived score is (\d+\.\d)\/4\.0, in the (critical|warning) band — no single finding drove this; it reflects the combined metric weighting below\.$/;
4467
4535
  var REMEDIATION_BY_METRIC = {
4468
4536
  test_to_source_ratio: "add concrete regression tests for the implementation surface",
@@ -4551,8 +4619,8 @@ function renderWitanHtmlReport(report, options = {}) {
4551
4619
  (criterion) => criterion.status === "verified" ? criterion.evidence.map((evidence) => renderEvidenceListItem(criterion, evidence)) : []
4552
4620
  );
4553
4621
  const openItems = report.criteria.flatMap(renderOpenItems);
4554
- const contributingSources = renderContributingSources(report.consumedSignals ?? []);
4555
- const externalSourceSummaries = summarizeExternalSources(report.consumedSignals ?? []);
4622
+ const externalSourceSummaries = summarizeSourcesByProvenance(report.consumedSignals ?? []);
4623
+ const contributingSources = externalSourceSummaries.map(renderSourceProvenanceLabel);
4556
4624
  const externalFindings = collectExternalFindings(report.consumedSignals ?? []);
4557
4625
  const coverage = computeMeasuredCoverage(report);
4558
4626
  const gitHistoryUnavailable = !report.repo.headSha;
@@ -4576,11 +4644,11 @@ function renderWitanHtmlReport(report, options = {}) {
4576
4644
  <p class="eyebrow">Trust Certificate</p>
4577
4645
  <h1>${escapeHtml(report.productDisplayName)}</h1>
4578
4646
  <dl class="meta">
4579
- <div><dt>Date</dt><dd>${escapeHtml(formatDate(report.generatedAt))}</dd></div>
4647
+ ${options.generatedAt ? `<div><dt>Date</dt><dd>${escapeHtml(formatDate(options.generatedAt))}</dd></div>` : ""}
4580
4648
  <div><dt>Run</dt><dd>${escapeHtml(renderRepo(report))}</dd></div>
4581
4649
  <div><dt>CLI</dt><dd>${escapeHtml(options.cliVersion ? `Cejel ${options.cliVersion}` : "Not recorded")}</dd></div>
4582
4650
  <div><dt>Rubric</dt><dd>${escapeHtml(report.rubricVersion)}</dd></div>
4583
- ${contributingSources.length > 0 ? `<div><dt>Sources</dt><dd>Incorporates findings from: ${escapeHtml(contributingSources.join(", "))}<ul class="source-counts">${externalSourceSummaries.map((s) => `<li>${escapeHtml(formatExternalSourceLine(s))}</li>`).join("")}</ul></dd></div>` : ""}
4651
+ ${contributingSources.length > 0 ? `<div><dt>Sources</dt><dd>Incorporates findings from: ${escapeHtml(contributingSources.join(", "))}<ul class="source-counts">${externalSourceSummaries.map((s) => `<li>${escapeHtml(formatSourceProvenanceLine(s))}</li>`).join("")}</ul></dd></div>` : ""}
4584
4652
  </dl>
4585
4653
  </section>
4586
4654
  <aside class="score-panel" aria-label="Summary trust score">
@@ -4598,6 +4666,15 @@ function renderWitanHtmlReport(report, options = {}) {
4598
4666
  </div>
4599
4667
  </header>
4600
4668
 
4669
+ ${(report.scanLimitations?.length ?? 0) > 0 ? `<section class="scan-limitations-section evidence-section" aria-label="Scan limitations">
4670
+ <h2>Scan limitations</h2>
4671
+ <ul>${(report.scanLimitations ?? []).map(
4672
+ (limitation) => `<li class="scan-warning"><strong>Scan limitation</strong><span>${escapeHtml(limitation)}</span></li>`
4673
+ ).join("")}</ul>
4674
+ </section>` : ""}
4675
+
4676
+ ${renderContentReadSummary(report)}
4677
+
4601
4678
  <section class="trust-grid" aria-label="Rubric criteria">
4602
4679
  ${renderCriterionColumn("Code trust", codeCriteria, gitHistoryUnavailable)}
4603
4680
  ${renderCriterionColumn("Process trust", processCriteria, gitHistoryUnavailable)}
@@ -4626,8 +4703,50 @@ function renderWitanHtmlReport(report, options = {}) {
4626
4703
  </html>
4627
4704
  `;
4628
4705
  }
4629
- function renderContributingSources(signals) {
4630
- return Array.from(new Set(signals.map((s) => s.source))).sort();
4706
+ function renderContentReadSummary(report) {
4707
+ const summary = report.contentReadSummary;
4708
+ if (!summary || summary.skipped === 0) return "";
4709
+ const errnoCounts = Object.entries(summary.unreadableByErrno).map(([errno, count]) => `${errno}: ${count}`).join(", ");
4710
+ const affected = summary.affectedCriteria.length > 0 ? `<p>Affected criteria abstained as <code>insufficient_data</code>: ${escapeHtml(summary.affectedCriteria.join(", "))}.</p>` : "";
4711
+ return `<section class="scan-limitations-section evidence-section" aria-label="Skipped content reads">
4712
+ <h2>Skipped content reads</h2>
4713
+ <p>${summary.skipped} content entr${summary.skipped === 1 ? "y was" : "ies were"} skipped. Counts only; repository paths are intentionally omitted.</p>
4714
+ <ul>
4715
+ <li>Unreadable: ${summary.byReason.unreadable}${errnoCounts ? ` (${escapeHtml(errnoCounts)})` : ""}</li>
4716
+ <li>Too large: ${summary.byReason.tooLarge}</li>
4717
+ <li>Excluded by extension: ${summary.byReason.excludedByExtension}</li>
4718
+ <li>Denied path: ${summary.byReason.deniedPath}</li>
4719
+ <li>Non-regular file: ${summary.byReason.nonRegularFile}</li>
4720
+ </ul>
4721
+ ${affected}
4722
+ </section>`;
4723
+ }
4724
+ function summarizeSourcesByProvenance(signals) {
4725
+ const grouped = /* @__PURE__ */ new Map();
4726
+ for (const signal of signals) {
4727
+ const provenance = signal.provenance ?? "operator_supplied";
4728
+ const key = `${signal.source}\0${provenance}`;
4729
+ const summary = grouped.get(key) ?? {
4730
+ source: signal.source,
4731
+ provenance,
4732
+ findingCount: 0,
4733
+ dimensions: /* @__PURE__ */ new Set()
4734
+ };
4735
+ summary.findingCount += signal.findingCount;
4736
+ summary.dimensions.add(signal.dimension);
4737
+ grouped.set(key, summary);
4738
+ }
4739
+ return Array.from(grouped.values()).map((summary) => ({ ...summary, dimensions: Array.from(summary.dimensions).sort() })).sort(
4740
+ (a, b) => a.source.localeCompare(b.source) || a.provenance.localeCompare(b.provenance)
4741
+ );
4742
+ }
4743
+ function renderSourceProvenanceLabel(summary) {
4744
+ const label = formatExternalSourceLabel(summary.source);
4745
+ return summary.provenance === "auto_discovered" ? `"${label}" (self-declared by the scanned repository \u2014 not verified)` : `${label} (operator-supplied)`;
4746
+ }
4747
+ function formatSourceProvenanceLine(summary) {
4748
+ const noun = summary.findingCount === 1 ? "finding" : "findings";
4749
+ return `${renderSourceProvenanceLabel(summary)}: ${summary.findingCount} ${noun} ingested (folded into ${summary.dimensions.join(", ")})`;
4631
4750
  }
4632
4751
  function renderExternalFindingItems(findings) {
4633
4752
  const shown = findings.slice(0, EXTERNAL_FINDINGS_DISPLAY_LIMIT);
@@ -4864,6 +4983,8 @@ dd { margin: 0; color: var(--muted); overflow-wrap: anywhere; }
4864
4983
  .coverage-note { margin-top: 8px; color: var(--faint); font-family: var(--mono); font-size: 11px; }
4865
4984
  .source-counts { margin: 8px 0 0; padding-left: 18px; font-family: var(--mono); font-size: 12px; color: var(--muted); }
4866
4985
  .trust-grid, .evidence-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; margin-top: 28px; }
4986
+ .scan-limitations-section { margin-top: 28px; border-color: rgba(231, 191, 114, .44); }
4987
+ .scan-limitations-section .scan-warning { list-style: none; margin-left: -18px; }
4867
4988
  .external-findings-section { margin-top: 18px; }
4868
4989
  .external-note { margin: 0 0 14px; color: var(--muted); font-size: 13px; }
4869
4990
  .criteria-column, .evidence-section { border: 1px solid var(--line); border-radius: 8px; background: var(--surface); padding: 22px; }
@@ -4913,6 +5034,7 @@ a:hover { text-decoration: underline; text-underline-offset: 2px; }
4913
5034
  `;
4914
5035
 
4915
5036
  // src/witan/badge.ts
5037
+ init_esm_shims();
4916
5038
  var VERDICT_COLOR = {
4917
5039
  Verified: "brightgreen",
4918
5040
  Conditional: "yellow",
@@ -4930,24 +5052,26 @@ var VERDICT_SVG_FILL = {
4930
5052
  "Insufficient evidence": "#8c8c8c"
4931
5053
  };
4932
5054
  function badgeMessage(report, verdict) {
5055
+ if ((report.scanLimitations?.length ?? 0) > 0) return "unrated: limited evidence";
4933
5056
  if (isWitanNoMeasurementAbstention(report)) return "unrated: no measurable evidence";
4934
5057
  if (report.verdict === "insufficient_source") return "unrated: no source";
4935
5058
  return `${formatScore2(report.overallScore)}/4.0 ${verdict.toLowerCase()}`;
4936
5059
  }
4937
5060
  function renderWitanBadgeEndpoint(report) {
4938
5061
  const verdict = renderReportVerdict(report);
5062
+ const limited = (report.scanLimitations?.length ?? 0) > 0;
4939
5063
  return {
4940
5064
  schemaVersion: 1,
4941
5065
  label: "cejel trust",
4942
5066
  message: badgeMessage(report, verdict),
4943
- color: VERDICT_COLOR[verdict] ?? "lightgrey"
5067
+ color: limited ? "lightgrey" : VERDICT_COLOR[verdict] ?? "lightgrey"
4944
5068
  };
4945
5069
  }
4946
5070
  function renderWitanBadgeSvg(report) {
4947
5071
  const verdict = renderReportVerdict(report);
4948
5072
  const label = "cejel trust";
4949
5073
  const message = badgeMessage(report, verdict);
4950
- const fill = VERDICT_SVG_FILL[verdict] ?? "#8c8c8c";
5074
+ const fill = (report.scanLimitations?.length ?? 0) > 0 ? "#8c8c8c" : VERDICT_SVG_FILL[verdict] ?? "#8c8c8c";
4951
5075
  const charWidth = 6.5;
4952
5076
  const padding = 10;
4953
5077
  const labelWidth = Math.round(label.length * charWidth + padding);
@@ -4983,6 +5107,7 @@ function escapeXml(value) {
4983
5107
  }
4984
5108
 
4985
5109
  // src/witan/attestation.ts
5110
+ init_esm_shims();
4986
5111
  import { createHash } from "crypto";
4987
5112
  var ATTESTATION_LIMITATIONS = [
4988
5113
  "Cejel measures observable repository signals; it is not a security or compliance guarantee.",
@@ -4996,6 +5121,19 @@ function hashWitanReport(report) {
4996
5121
  }
4997
5122
  function createWitanAttestation(report, options) {
4998
5123
  const reportSha256 = hashWitanReport(report);
5124
+ const externalSignalProvenance = Array.from(
5125
+ new Map(
5126
+ (report.consumedSignals ?? []).map((signal) => {
5127
+ const provenance = signal.provenance ?? "operator_supplied";
5128
+ return [
5129
+ `${signal.source}\0${provenance}`,
5130
+ { source: signal.source, provenance }
5131
+ ];
5132
+ })
5133
+ ).values()
5134
+ ).sort(
5135
+ (a, b) => a.source.localeCompare(b.source) || a.provenance.localeCompare(b.provenance)
5136
+ );
4999
5137
  const outcome = report.verdict === "insufficient_source" ? {
5000
5138
  status: "abstained",
5001
5139
  reason: report.insufficientSourceReason
@@ -5016,7 +5154,7 @@ function createWitanAttestation(report, options) {
5016
5154
  predicateType: WITAN_ATTESTATION_PREDICATE_TYPE,
5017
5155
  predicate: {
5018
5156
  tool: { name: "cejel", version: options.toolVersion },
5019
- generatedAt: report.generatedAt,
5157
+ generatedAt: options.generatedAt,
5020
5158
  rubricVersion: report.rubricVersion,
5021
5159
  repository: {
5022
5160
  productSlug: report.productSlug,
@@ -5027,6 +5165,7 @@ function createWitanAttestation(report, options) {
5027
5165
  artifact: "report.json",
5028
5166
  sha256: reportSha256
5029
5167
  },
5168
+ externalSignalProvenance,
5030
5169
  outcome,
5031
5170
  assurance: {
5032
5171
  status: "unsigned",
@@ -5059,9 +5198,6 @@ function verifyWitanAttestationBinding(statement, report, options = {}) {
5059
5198
  if (parsed.data.predicate.rubricVersion !== report.rubricVersion) {
5060
5199
  errors.push("rubric version does not match report.json");
5061
5200
  }
5062
- if (parsed.data.predicate.generatedAt !== report.generatedAt) {
5063
- errors.push("generated timestamp does not match report.json");
5064
- }
5065
5201
  if (parsed.data.predicate.repository.headSha !== report.repo.headSha) {
5066
5202
  errors.push("repository revision does not match report.json");
5067
5203
  }
@@ -5071,6 +5207,22 @@ function verifyWitanAttestationBinding(statement, report, options = {}) {
5071
5207
  if (parsed.data.predicate.repository.url !== report.repo.url) {
5072
5208
  errors.push("repository URL does not match report.json");
5073
5209
  }
5210
+ const expectedExternalSignalProvenance = Array.from(
5211
+ new Map(
5212
+ (report.consumedSignals ?? []).map((signal) => {
5213
+ const provenance = signal.provenance ?? "operator_supplied";
5214
+ return [
5215
+ `${signal.source}\0${provenance}`,
5216
+ { source: signal.source, provenance }
5217
+ ];
5218
+ })
5219
+ ).values()
5220
+ ).sort(
5221
+ (a, b) => a.source.localeCompare(b.source) || a.provenance.localeCompare(b.provenance)
5222
+ );
5223
+ if (JSON.stringify(parsed.data.predicate.externalSignalProvenance) !== JSON.stringify(expectedExternalSignalProvenance)) {
5224
+ errors.push("external signal provenance does not match report.json");
5225
+ }
5074
5226
  const outcome = parsed.data.predicate.outcome;
5075
5227
  if (report.verdict === "insufficient_source") {
5076
5228
  if (outcome.status !== "abstained" || outcome.reason !== report.insufficientSourceReason) {
@@ -5082,15 +5234,17 @@ function verifyWitanAttestationBinding(statement, report, options = {}) {
5082
5234
  return { valid: errors.length === 0, errors };
5083
5235
  }
5084
5236
 
5237
+ // src/witan/index.ts
5238
+ init_esm_shims();
5239
+
5240
+ // src/witan/markdown.ts
5241
+ init_esm_shims();
5242
+
5085
5243
  // src/witan/repo-signals.ts
5086
- import { execFileSync } from "child_process";
5244
+ init_esm_shims();
5087
5245
  import { createHash as createHash2 } from "crypto";
5088
5246
  import {
5089
- closeSync,
5090
5247
  lstatSync,
5091
- openSync,
5092
- readFileSync,
5093
- readSync,
5094
5248
  readdirSync,
5095
5249
  realpathSync,
5096
5250
  statSync
@@ -5098,11 +5252,13 @@ import {
5098
5252
  import { basename, dirname, extname, join, relative, resolve, sep } from "path";
5099
5253
 
5100
5254
  // src/witan/json-safe.ts
5255
+ init_esm_shims();
5101
5256
  function stripBom(text) {
5102
5257
  return text.charCodeAt(0) === 65279 ? text.slice(1) : text;
5103
5258
  }
5104
5259
 
5105
5260
  // src/witan/rubric-version.ts
5261
+ init_esm_shims();
5106
5262
  var WITAN_RUBRIC_VERSION_V9 = "witan-rubric-v9-2026-07-22";
5107
5263
  var WITAN_RUBRIC_VERSION_V10 = "witan-rubric-v10-2026-07-23";
5108
5264
  var WITAN_RUBRIC_VERSION_V11 = "witan-rubric-v11-2026-07-23";
@@ -5119,12 +5275,41 @@ function usesV17DetectorClosure(rubricVersion) {
5119
5275
  return rubricVersion === WITAN_RUBRIC_VERSION_V17 || rubricVersion === WITAN_RUBRIC_VERSION_V18;
5120
5276
  }
5121
5277
  function buildWitanInputFromRepo(options) {
5278
+ const tracked = trackContentReads(options.repoPath, () => buildWitanInputFromRepoUntracked(options));
5279
+ const affectedCriteria = tracked.affectedCriteria;
5280
+ const signals = (tracked.value.signals ?? []).map((signal) => {
5281
+ if (!affectedCriteria.has(signal.criterionId)) return signal;
5282
+ return {
5283
+ criterionId: signal.criterionId,
5284
+ positiveEvidence: [],
5285
+ findings: [],
5286
+ metrics: [],
5287
+ insufficientData: true,
5288
+ 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."
5289
+ };
5290
+ });
5291
+ const affectedCount = affectedCriteria.size;
5292
+ const scanLimitations = [...tracked.value.scanLimitations ?? []];
5293
+ if (affectedCount > 0 && scanLimitations.length < 16) {
5294
+ scanLimitations.push(
5295
+ `${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.`
5296
+ );
5297
+ }
5298
+ return {
5299
+ ...tracked.value,
5300
+ signals,
5301
+ ...scanLimitations.length > 0 ? { scanLimitations } : {},
5302
+ contentReadSummary: tracked.summary
5303
+ };
5304
+ }
5305
+ function buildWitanInputFromRepoUntracked(options) {
5122
5306
  assertRepoPathExists(options.repoPath);
5123
5307
  const generatedAt = options.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
5124
5308
  const rubricVersion = options.rubricVersion ?? WITAN_RUBRIC_VERSION;
5309
+ const fileInventory = collectRepoFileInventory(options.repoPath);
5125
5310
  const headSha = readGitHead(options.repoPath);
5126
- const repoFiles = listRepoFiles(options.repoPath);
5127
- const inventoryFiles = listRepoInventory(options.repoPath, repoFiles);
5311
+ const repoFiles = fileInventory.repoFiles;
5312
+ const inventoryFiles = fileInventory.inventoryFiles;
5128
5313
  const ignoredTargetReason = repoFiles.length === 0 ? explainIgnoredScanTarget(options.repoPath) : void 0;
5129
5314
  const usesV17Detectors = usesV17DetectorClosure(rubricVersion);
5130
5315
  const usesV18NativeRls = rubricVersion === WITAN_RUBRIC_VERSION_V18;
@@ -5148,6 +5333,7 @@ function buildWitanInputFromRepo(options) {
5148
5333
  const usesV39Detectors = 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 || usesV17DetectorClosure(rubricVersion);
5149
5334
  const usesV47Detectors = rubricVersion === WITAN_RUBRIC_VERSION_V15 || rubricVersion === WITAN_RUBRIC_VERSION_V16 || usesV17DetectorClosure(rubricVersion);
5150
5335
  const reviewableSourceProof = usesV17Detectors ? buildReviewableSourceProof(options.repoPath, inventoryFiles, headSha, rubricVersion) : void 0;
5336
+ const scanLimitations = new Set(fileInventory.scanLimitations);
5151
5337
  const coreSignals = collectRepoSignals(
5152
5338
  options.repoPath,
5153
5339
  generatedAt,
@@ -5158,7 +5344,8 @@ function buildWitanInputFromRepo(options) {
5158
5344
  usesV39Detectors,
5159
5345
  usesV47Detectors,
5160
5346
  usesV18NativeRls,
5161
- reviewableSourceProof
5347
+ reviewableSourceProof,
5348
+ scanLimitations
5162
5349
  );
5163
5350
  const archetype = usesV17Detectors && reviewableSourceProof?.passes === true && isV17RescuableAbstention(gatedArchetype.abstentionKind) && coreSignals.some(isMeasuredCriterionSignal) ? {
5164
5351
  archetype: isLikelyMonorepo(inventoryFiles) ? "monorepo" : "source",
@@ -5176,6 +5363,7 @@ function buildWitanInputFromRepo(options) {
5176
5363
  ...options.rubricVersion ? { rubricVersion: options.rubricVersion } : {},
5177
5364
  archetype: archetype.archetype,
5178
5365
  ...ignoredTargetReason ?? archetype.insufficientSourceReason ? { insufficientSourceReason: ignoredTargetReason ?? archetype.insufficientSourceReason } : {},
5366
+ ...scanLimitations.size > 0 ? { scanLimitations: [...scanLimitations] } : {},
5179
5367
  signals: [
5180
5368
  ...coreSignals,
5181
5369
  ...(options.domainCollectors ?? []).map((collect) => collect(options.repoPath, repoFiles)),
@@ -5648,26 +5836,21 @@ function isRubyPackageRecipeMetadataSource(repoPath, file) {
5648
5836
  function readSemanticMetadataPrefix(repoPath, file) {
5649
5837
  const absolutePath = join(repoPath, file);
5650
5838
  if (!isRegularFile(absolutePath)) return null;
5651
- let descriptor = null;
5652
- try {
5653
- descriptor = openSync(absolutePath, "r");
5654
- const buffer = Buffer.allocUnsafe(SEMANTIC_METADATA_PREFIX_BYTES_V14);
5655
- const bytesRead = readSync(descriptor, buffer, 0, SEMANTIC_METADATA_PREFIX_BYTES_V14, 0);
5656
- return buffer.subarray(0, bytesRead).toString("utf8");
5657
- } catch {
5658
- return null;
5659
- } finally {
5660
- if (descriptor !== null) closeSync(descriptor);
5661
- }
5839
+ return readRepoTextPrefix(absolutePath, SEMANTIC_METADATA_PREFIX_BYTES_V14);
5662
5840
  }
5663
5841
  function readRepresentativeSourceText(repoPath, file) {
5664
5842
  const absolutePath = join(repoPath, file);
5665
5843
  try {
5666
5844
  if (!isRegularFile(absolutePath)) return null;
5667
5845
  const size = statSync(absolutePath).size;
5668
- if (size === 0 || size > READABLE_SOURCE_REPRESENTATION_FILE_BYTES_V13) return null;
5669
- return readFileSync(absolutePath, "utf8");
5670
- } catch {
5846
+ if (size === 0) return null;
5847
+ if (size > READABLE_SOURCE_REPRESENTATION_FILE_BYTES_V13) {
5848
+ recordContentSkip(absolutePath, "too_large", true);
5849
+ return null;
5850
+ }
5851
+ return readRepoText(absolutePath, "utf8");
5852
+ } catch (error) {
5853
+ recordFilesystemSkip(absolutePath, error, false, true);
5671
5854
  return null;
5672
5855
  }
5673
5856
  }
@@ -5737,28 +5920,25 @@ function assertRepoPathExists(repoPath) {
5737
5920
  }
5738
5921
  }
5739
5922
  function explainIgnoredScanTarget(repoPath) {
5740
- let top;
5741
- try {
5742
- top = execFileSync("git", ["rev-parse", "--show-toplevel"], {
5743
- cwd: repoPath,
5744
- encoding: "utf8",
5745
- stdio: ["ignore", "pipe", "ignore"]
5746
- }).trim();
5747
- } catch {
5748
- return void 0;
5923
+ const topResult = execGit(["rev-parse", "--show-toplevel"], { cwd: repoPath });
5924
+ if (!topResult.ok) {
5925
+ if (isExpectedGitAbsence(topResult)) return void 0;
5926
+ throw new Error(describeGitFailure(topResult.reason, "repository-root discovery"));
5749
5927
  }
5928
+ const top = topResult.stdout.trim();
5750
5929
  if (!top) return void 0;
5751
5930
  const topResolved = realpathSync(top);
5752
5931
  const targetResolved = realpathSync(repoPath);
5753
5932
  const target = relative(topResolved, targetResolved).split(sep).join("/");
5754
5933
  if (!target || target.startsWith("../")) return void 0;
5755
- try {
5756
- execFileSync("git", ["check-ignore", "--quiet", "--", `${target}/`], {
5757
- cwd: topResolved,
5758
- stdio: "ignore"
5759
- });
5760
- } catch {
5761
- return void 0;
5934
+ const ignored = execGit(["check-ignore", "--quiet", "--", `${target}/`], {
5935
+ cwd: topResolved
5936
+ });
5937
+ if (!ignored.ok) {
5938
+ if (isExpectedGitAbsence(ignored) || ignored.reason === "exec_failed" && ignored.exitCode === 1) {
5939
+ return void 0;
5940
+ }
5941
+ throw new Error(describeGitFailure(ignored.reason, "ignore-rule check"));
5762
5942
  }
5763
5943
  return `The requested scan path (${target}) is excluded by this repository's Git ignore rules, so Cejel found no tracked source to evaluate. Scan a tracked project or intentionally add the path to version control; Cejel abstains rather than score an ignored or vendored working tree.`;
5764
5944
  }
@@ -5772,48 +5952,74 @@ function buildNotApplicableSignal(criterionId, reason) {
5772
5952
  notApplicable: true
5773
5953
  };
5774
5954
  }
5775
- function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV18NativeRls, reviewableSourceProof) {
5955
+ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV18NativeRls, reviewableSourceProof, scanLimitations = /* @__PURE__ */ new Set()) {
5776
5956
  const signals = [];
5777
- const mono = resolveMonorepoContext(repoPath);
5778
- const a1Signal = collectA1TestIntegrityEvidence(
5779
- repoPath,
5780
- repoFiles,
5781
- useV27Detectors,
5782
- useV33Detectors,
5783
- useV36Detectors,
5784
- reviewableSourceProof
5957
+ const mono = resolveMonorepoContext(repoPath, scanLimitations);
5958
+ const collectCriterion = (criterionId, collect) => withContentReadCriterion(criterionId, collect);
5959
+ const a1Signal = collectCriterion(
5960
+ "A1",
5961
+ () => collectA1TestIntegrityEvidence(
5962
+ repoPath,
5963
+ repoFiles,
5964
+ useV27Detectors,
5965
+ useV33Detectors,
5966
+ useV36Detectors,
5967
+ reviewableSourceProof
5968
+ )
5785
5969
  );
5786
- const a2Signal = collectA2IsolationEvidence(
5787
- repoPath,
5788
- repoFiles,
5789
- useV27Detectors,
5790
- useV33Detectors,
5791
- useV36Detectors,
5792
- useV39Detectors,
5793
- useV47Detectors,
5794
- useV18NativeRls
5970
+ const a2Signal = collectCriterion(
5971
+ "A2",
5972
+ () => collectA2IsolationEvidence(
5973
+ repoPath,
5974
+ repoFiles,
5975
+ useV27Detectors,
5976
+ useV33Detectors,
5977
+ useV36Detectors,
5978
+ useV39Detectors,
5979
+ useV47Detectors,
5980
+ useV18NativeRls,
5981
+ scanLimitations
5982
+ )
5795
5983
  );
5796
- const a3Signal = collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors);
5797
- const a4Signal = collectA4DependencyEvidence(
5798
- repoPath,
5799
- repoFiles,
5800
- mono,
5801
- useV27Detectors,
5802
- useV47Detectors
5984
+ const a3Signal = collectCriterion(
5985
+ "A3",
5986
+ () => collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors)
5987
+ );
5988
+ const a4Signal = collectCriterion(
5989
+ "A4",
5990
+ () => collectA4DependencyEvidence(
5991
+ repoPath,
5992
+ repoFiles,
5993
+ mono,
5994
+ useV27Detectors,
5995
+ useV47Detectors
5996
+ )
5997
+ );
5998
+ const a5Signal = collectCriterion(
5999
+ "A5",
6000
+ () => collectA5ClaimRealityEvidence(repoPath, repoFiles, useV27Detectors)
5803
6001
  );
5804
- const a5Signal = collectA5ClaimRealityEvidence(repoPath, repoFiles, useV27Detectors);
5805
6002
  const b1Signal = buildNotApplicableSignal(
5806
6003
  "B1",
5807
6004
  "Repository scans do not evaluate the dispatch-trace process dimension."
5808
6005
  );
5809
- const b2Signal = collectB2PrTraceEvidence(repoPath, repoFiles);
5810
- const b3Signal = collectB3CiDisciplineEvidence(repoPath, repoFiles, mono);
5811
- const b4Signal = collectB4AuditEvidence(repoPath, repoFiles, generatedAt);
6006
+ const b2Signal = collectCriterion("B2", () => collectB2PrTraceEvidence(repoPath, repoFiles));
6007
+ const b3Signal = collectCriterion(
6008
+ "B3",
6009
+ () => collectB3CiDisciplineEvidence(repoPath, repoFiles, mono)
6010
+ );
6011
+ const b4Signal = collectCriterion(
6012
+ "B4",
6013
+ () => collectB4AuditEvidence(repoPath, repoFiles, generatedAt)
6014
+ );
5812
6015
  const b5Signal = buildNotApplicableSignal(
5813
6016
  "B5",
5814
6017
  "Repository scans do not evaluate the learning-trace process dimension."
5815
6018
  );
5816
- const b6Signal = collectB6PrivilegedOpsGatingEvidence(repoPath, repoFiles, useV39Detectors);
6019
+ const b6Signal = collectCriterion(
6020
+ "B6",
6021
+ () => collectB6PrivilegedOpsGatingEvidence(repoPath, repoFiles, useV39Detectors)
6022
+ );
5817
6023
  for (const signal of [
5818
6024
  a1Signal,
5819
6025
  a2Signal,
@@ -6083,7 +6289,7 @@ function buildA1AuthenticatedAbsenceSignal(repoPath, proof) {
6083
6289
  notes: `V17 authenticated absence: ${proof.eligibleSourceFileCount} criterion-ratable of ${proof.sourceShapedFileCount} source-shaped files; sampled ${proof.sampledFileCount}; ${proof.readableSampledFileCount} representative-text files.`
6084
6290
  };
6085
6291
  }
6086
- function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV18NativeRls) {
6292
+ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV18NativeRls, scanLimitations) {
6087
6293
  const evidence = [];
6088
6294
  const findings = [];
6089
6295
  const gitignore = repoFiles.find((file) => basename(file) === ".gitignore");
@@ -6129,6 +6335,7 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
6129
6335
  repoPath,
6130
6336
  useV27Detectors,
6131
6337
  currentSecretFingerprintsByPath,
6338
+ scanLimitations,
6132
6339
  useV33Detectors,
6133
6340
  useV36Detectors,
6134
6341
  useV39Detectors,
@@ -6145,7 +6352,7 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
6145
6352
  const committedEnvFilePath = repoFiles.find(
6146
6353
  (f) => /(?:^|\/)\.env(?:\.|$)/i.test(f) && !isEnvTemplatePath(f, useV47Detectors) && (!useV47Detectors || isV47AuthoredProductionPath(f)) && (!useV39Detectors || !hasTemplateOnlyEnvContent(repoPath, f))
6147
6354
  );
6148
- const hasEnvInHistory = committedEnvFilePath === void 0 && hasEnvPathInGitHistory(repoPath);
6355
+ const hasEnvInHistory = committedEnvFilePath === void 0 && hasEnvPathInGitHistory(repoPath, scanLimitations);
6149
6356
  const dbLayerImportFile = migrationFiles.length === 0 ? implFiles.slice(0, 30).find((f) => fileContains(repoPath, f, DB_CLIENT_PATTERN)) : void 0;
6150
6357
  const hasDataLayer = migrationFiles.length > 0 || dbLayerImportFile !== void 0;
6151
6358
  const hasSecretsSurface = envExamples.length > 0 || committedEnvFilePath !== void 0 || gitignoreHasEnvRule || hasEnvInHistory;
@@ -7305,14 +7512,14 @@ function lineHasInsecureSecretCompare(line, useV39Detectors = false) {
7305
7512
  function findInsecureSecretCompareLine(repoPath, file, useV39Detectors = false) {
7306
7513
  const fullPath = join(repoPath, file);
7307
7514
  if (!isRegularFile(fullPath)) return null;
7308
- const lines = readFileSync(fullPath, "utf8").split("\n");
7515
+ const lines = readRepoText(fullPath, "utf8").split("\n");
7309
7516
  const index = lines.findIndex((line) => lineHasInsecureSecretCompare(line, useV39Detectors));
7310
7517
  return index === -1 ? null : index + 1;
7311
7518
  }
7312
7519
  function findFirstMatchingLine(repoPath, file, pattern) {
7313
7520
  const fullPath = join(repoPath, file);
7314
7521
  if (!isRegularFile(fullPath)) return null;
7315
- const lines = readFileSync(fullPath, "utf8").split("\n");
7522
+ const lines = readRepoText(fullPath, "utf8").split("\n");
7316
7523
  const index = lines.findIndex((line) => pattern.test(line));
7317
7524
  return index === -1 ? null : index + 1;
7318
7525
  }
@@ -7320,7 +7527,7 @@ function lineAt(repoPath, file, line) {
7320
7527
  if (line < 1) return null;
7321
7528
  const fullPath = join(repoPath, file);
7322
7529
  if (!isRegularFile(fullPath)) return null;
7323
- return readFileSync(fullPath, "utf8").split("\n")[line - 1] ?? null;
7530
+ return readRepoText(fullPath, "utf8").split("\n")[line - 1] ?? null;
7324
7531
  }
7325
7532
  var UNSORTED_JSON_SIGN_PATTERN = /\.update\s*\(\s*JSON\.stringify\s*\(|\.sign\s*\([^)]*JSON\.stringify\s*\(/;
7326
7533
  var UNSORTED_JSON_DIRECT_SIGN_PATTERN = /\.sign\s*\([^)]*JSON\.stringify\s*\(/;
@@ -7329,7 +7536,7 @@ var JSON_STRINGIFY_ASSIGNMENT_PATTERN = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*
7329
7536
  function findUnsortedJsonHmacLine(repoPath, file) {
7330
7537
  const fullPath = join(repoPath, file);
7331
7538
  if (!isRegularFile(fullPath)) return null;
7332
- const contents = readFileSync(fullPath, "utf8");
7539
+ const contents = readRepoText(fullPath, "utf8");
7333
7540
  const directSign = findFirstMatchingLine(repoPath, file, UNSORTED_JSON_DIRECT_SIGN_PATTERN);
7334
7541
  if (directSign !== null) return directSign;
7335
7542
  const directConstructorUpdate = findFirstMatchingLine(
@@ -7410,27 +7617,92 @@ function isEnvTemplatePath(path, useV47Detectors = false) {
7410
7617
  }
7411
7618
  var HARD_EXCLUDED_PATH_PATTERN = /(^|\/)(?:\.git|\.venv|venv|site-packages|node_modules|dist|build|\.next|__pycache__|vendor|\.terraform|coverage)(?:\/|$)/;
7412
7619
  var COVERAGE_PERCENT_PATTERN = /(?:statements|branches|lines|functions|fail_under)\s*[:=]\s*["']?(\d+(?:\.\d+)?)/gi;
7413
- function listRepoFiles(repoPath) {
7414
- const gitFiles = listGitTrackedFiles(repoPath, false);
7415
- if (gitFiles) return gitFiles;
7620
+ function gitFailureDetail(reason) {
7621
+ if (reason === "buffer_exceeded") return "output exceeded the explicit 8 MiB limit";
7622
+ if (reason === "timeout") return "execution exceeded the explicit 30 second timeout";
7623
+ return "the local Git command failed";
7624
+ }
7625
+ function readGitTrackedFileOutput(repoPath) {
7626
+ const workTree = execGit(["rev-parse", "--is-inside-work-tree"], { cwd: repoPath });
7627
+ if (!workTree.ok) return workTree;
7628
+ if (workTree.stdout.trim() !== "true") return { ok: false, reason: "not_a_repo" };
7629
+ return execGit(["ls-files", "--cached"], { cwd: repoPath });
7630
+ }
7631
+ function parseGitTrackedFiles(repoPath, output, includeHardExcluded) {
7416
7632
  const files = [];
7417
- visitRepoDir(repoPath, repoPath, files);
7633
+ for (const file of output.trim().split("\n")) {
7634
+ if (file.length === 0) continue;
7635
+ const fullPath = join(repoPath, file);
7636
+ if (!includeHardExcluded && isHardExcludedPath(file)) {
7637
+ recordContentSkip(fullPath, "denied_path", true);
7638
+ continue;
7639
+ }
7640
+ try {
7641
+ if (!lstatSync(fullPath).isFile()) {
7642
+ recordContentSkip(fullPath, "non_regular_file", true);
7643
+ continue;
7644
+ }
7645
+ } catch (error) {
7646
+ recordFilesystemSkip(fullPath, error, false, true);
7647
+ continue;
7648
+ }
7649
+ if (!isPotentialContentPath(file)) {
7650
+ recordContentSkip(fullPath, "excluded_by_extension", true);
7651
+ }
7652
+ files.push(file);
7653
+ }
7654
+ return files.sort();
7655
+ }
7656
+ function directoryInventory(repoPath) {
7657
+ const files = [];
7658
+ const rootDevice = statSync(repoPath).dev;
7659
+ visitRepoDir(repoPath, repoPath, files, rootDevice);
7418
7660
  return files.filter((file) => !isHardExcludedPath(file)).sort();
7419
7661
  }
7420
- function listRepoInventory(repoPath, fallbackFiles) {
7421
- return listGitTrackedFiles(repoPath, true) ?? [...fallbackFiles];
7662
+ function collectRepoFileInventory(repoPath) {
7663
+ const tracked = readGitTrackedFileOutput(repoPath);
7664
+ if (tracked.ok) {
7665
+ return {
7666
+ repoFiles: parseGitTrackedFiles(repoPath, tracked.stdout, false),
7667
+ inventoryFiles: parseGitTrackedFiles(repoPath, tracked.stdout, true),
7668
+ scanLimitations: []
7669
+ };
7670
+ }
7671
+ const fallbackFiles = directoryInventory(repoPath);
7672
+ if (isExpectedGitAbsence(tracked)) {
7673
+ return {
7674
+ repoFiles: fallbackFiles,
7675
+ inventoryFiles: fallbackFiles,
7676
+ scanLimitations: []
7677
+ };
7678
+ }
7679
+ return {
7680
+ repoFiles: fallbackFiles,
7681
+ inventoryFiles: fallbackFiles,
7682
+ scanLimitations: [
7683
+ `Tracked-file inventory was unavailable because ${gitFailureDetail(tracked.reason)}. Cejel used a bounded directory walk with different exclusion semantics; re-scan after the local Git failure is resolved to compare the certificate.`
7684
+ ]
7685
+ };
7422
7686
  }
7423
- function resolveMonorepoContext(repoPath) {
7424
- let top;
7425
- try {
7426
- top = execFileSync("git", ["rev-parse", "--show-toplevel"], {
7427
- cwd: repoPath,
7428
- encoding: "utf8",
7429
- stdio: ["ignore", "pipe", "ignore"]
7430
- }).trim();
7431
- } catch {
7687
+ function listRepoFiles(repoPath) {
7688
+ return [...collectRepoFileInventory(repoPath).repoFiles];
7689
+ }
7690
+ function listRepoInventory(repoPath, fallbackFiles) {
7691
+ const tracked = readGitTrackedFileOutput(repoPath);
7692
+ if (tracked.ok) return parseGitTrackedFiles(repoPath, tracked.stdout, true);
7693
+ if (isExpectedGitAbsence(tracked)) return [...fallbackFiles];
7694
+ throw new Error(describeGitFailure(tracked.reason, "tracked-file inventory"));
7695
+ }
7696
+ function resolveMonorepoContext(repoPath, scanLimitations) {
7697
+ const topResult = execGit(["rev-parse", "--show-toplevel"], { cwd: repoPath });
7698
+ if (!topResult.ok) {
7699
+ if (isExpectedGitAbsence(topResult)) return null;
7700
+ scanLimitations.add(
7701
+ `${describeGitFailure(topResult.reason, "repository-root discovery")} Cejel continued without monorepo root-level shared configuration.`
7702
+ );
7432
7703
  return null;
7433
7704
  }
7705
+ const top = topResult.stdout.trim();
7434
7706
  if (!top) return null;
7435
7707
  const rootResolved = resolve(top);
7436
7708
  const repoResolved = (() => {
@@ -7442,7 +7714,7 @@ function resolveMonorepoContext(repoPath) {
7442
7714
  })();
7443
7715
  if (rootResolved === repoResolved) return null;
7444
7716
  if (!repoResolved.startsWith(rootResolved + sep)) return null;
7445
- const rootFiles = listGitTrackedFiles(rootResolved);
7717
+ const rootFiles = listGitTrackedFiles(rootResolved, scanLimitations);
7446
7718
  if (!rootFiles || rootFiles.length === 0) return null;
7447
7719
  const subRel = repoResolved.slice(rootResolved.length + 1).split(sep).join("/");
7448
7720
  const subPrefix = subRel ? `${subRel}/` : "";
@@ -7452,50 +7724,119 @@ function resolveMonorepoContext(repoPath) {
7452
7724
  if (sharedFiles.length === 0) return null;
7453
7725
  return { root: rootResolved, sharedFiles };
7454
7726
  }
7455
- function listGitTrackedFiles(repoPath, includeHardExcluded = false) {
7456
- try {
7457
- execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
7458
- cwd: repoPath,
7459
- encoding: "utf8",
7460
- stdio: ["ignore", "pipe", "ignore"]
7461
- });
7462
- const output = execFileSync("git", ["ls-files", "--cached"], {
7463
- cwd: repoPath,
7464
- encoding: "utf8",
7465
- stdio: ["ignore", "pipe", "ignore"]
7466
- }).trim();
7467
- if (!output) return [];
7468
- return output.split("\n").filter(
7469
- (file) => file.length > 0 && (includeHardExcluded || !isHardExcludedPath(file)) && isRegularFile(join(repoPath, file))
7470
- ).sort();
7471
- } catch {
7472
- return null;
7473
- }
7727
+ function listGitTrackedFiles(repoPath, scanLimitations, includeHardExcluded = false) {
7728
+ const tracked = readGitTrackedFileOutput(repoPath);
7729
+ if (tracked.ok) return parseGitTrackedFiles(repoPath, tracked.stdout, includeHardExcluded);
7730
+ if (isExpectedGitAbsence(tracked)) return null;
7731
+ scanLimitations.add(
7732
+ `${describeGitFailure(tracked.reason, "monorepo shared-file inventory")} Cejel continued without monorepo root-level shared configuration.`
7733
+ );
7734
+ return null;
7474
7735
  }
7475
- function visitRepoDir(repoPath, dirPath, files) {
7736
+ function visitRepoDir(repoPath, dirPath, files, rootDevice) {
7476
7737
  let entries;
7477
7738
  try {
7478
7739
  entries = readdirSync(dirPath, { withFileTypes: true });
7479
- } catch {
7740
+ } catch (error) {
7741
+ recordFilesystemSkip(dirPath, error, true);
7480
7742
  return;
7481
7743
  }
7482
7744
  for (const entry of entries) {
7483
- if (shouldSkipDir(entry.name)) continue;
7484
7745
  const fullPath = join(dirPath, entry.name);
7746
+ if (shouldSkipDir(entry.name)) {
7747
+ recordContentSkip(fullPath, "denied_path");
7748
+ continue;
7749
+ }
7485
7750
  if (entry.isDirectory()) {
7486
- visitRepoDir(repoPath, fullPath, files);
7751
+ try {
7752
+ if (lstatSync(fullPath, { bigint: true }).dev !== BigInt(rootDevice)) {
7753
+ recordContentSkip(fullPath, "denied_path");
7754
+ continue;
7755
+ }
7756
+ } catch (error) {
7757
+ recordFilesystemSkip(fullPath, error);
7758
+ continue;
7759
+ }
7760
+ visitRepoDir(repoPath, fullPath, files, rootDevice);
7761
+ continue;
7762
+ }
7763
+ if (!entry.isFile()) {
7764
+ recordContentSkip(fullPath, "non_regular_file");
7487
7765
  continue;
7488
7766
  }
7489
- if (!entry.isFile()) continue;
7490
7767
  let size;
7491
7768
  try {
7492
7769
  size = statSync(fullPath).size;
7493
- } catch {
7770
+ } catch (error) {
7771
+ recordFilesystemSkip(fullPath, error);
7494
7772
  continue;
7495
7773
  }
7496
- if (size > 512e3) continue;
7497
- files.push(relative(repoPath, fullPath));
7498
- }
7774
+ const repoRelativePath = relative(repoPath, fullPath);
7775
+ if (size > 512e3) {
7776
+ recordContentSkip(fullPath, "too_large");
7777
+ continue;
7778
+ }
7779
+ if (!isPotentialContentPath(repoRelativePath)) {
7780
+ recordContentSkip(fullPath, "excluded_by_extension");
7781
+ }
7782
+ files.push(repoRelativePath);
7783
+ }
7784
+ }
7785
+ var CONTENT_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
7786
+ ".c",
7787
+ ".cc",
7788
+ ".cfg",
7789
+ ".cjs",
7790
+ ".cmake",
7791
+ ".conf",
7792
+ ".cpp",
7793
+ ".cs",
7794
+ ".css",
7795
+ ".cts",
7796
+ ".cxx",
7797
+ ".env",
7798
+ ".go",
7799
+ ".gradle",
7800
+ ".graphql",
7801
+ ".h",
7802
+ ".hpp",
7803
+ ".html",
7804
+ ".ini",
7805
+ ".java",
7806
+ ".js",
7807
+ ".json",
7808
+ ".json5",
7809
+ ".jsx",
7810
+ ".kt",
7811
+ ".kts",
7812
+ ".lua",
7813
+ ".md",
7814
+ ".mdx",
7815
+ ".mjs",
7816
+ ".mts",
7817
+ ".php",
7818
+ ".properties",
7819
+ ".proto",
7820
+ ".py",
7821
+ ".rb",
7822
+ ".rs",
7823
+ ".rst",
7824
+ ".scss",
7825
+ ".sh",
7826
+ ".sql",
7827
+ ".swift",
7828
+ ".toml",
7829
+ ".ts",
7830
+ ".tsx",
7831
+ ".txt",
7832
+ ".xml",
7833
+ ".yaml",
7834
+ ".yml"
7835
+ ]);
7836
+ function isPotentialContentPath(path) {
7837
+ const name = basename(path);
7838
+ if (CONTENT_FILE_EXTENSIONS.has(extname(name).toLowerCase())) return true;
7839
+ return /^(?:Dockerfile|Makefile|Rakefile|Gemfile|Pipfile|Procfile|CMakeLists\.txt)$/i.test(name) || /^\.(?:env|gitignore|npmrc|nvmrc|prettierrc|eslintrc)(?:\.|$)/i.test(name);
7499
7840
  }
7500
7841
  function shouldSkipDir(name) {
7501
7842
  return [
@@ -7522,7 +7863,7 @@ function collectContentBasedCppTestFiles(repoPath, repoFiles) {
7522
7863
  return candidates.filter((file) => {
7523
7864
  const fullPath = join(repoPath, file);
7524
7865
  if (!isRegularFile(fullPath)) return false;
7525
- const contents = readFileSync(fullPath, "utf8");
7866
+ const contents = readRepoText(fullPath, "utf8");
7526
7867
  return CPP_TEST_FRAMEWORK_PATTERN.test(contents) || CPP_TEST_MACRO_PATTERN.test(contents);
7527
7868
  });
7528
7869
  }
@@ -7542,7 +7883,7 @@ function measureNonHollowTestShare(repoPath, files, useV17Detectors = false) {
7542
7883
  for (const file of files) {
7543
7884
  const fullPath = join(repoPath, file);
7544
7885
  if (!isRegularFile(fullPath)) continue;
7545
- const contents = readFileSync(fullPath, "utf8");
7886
+ const contents = readRepoText(fullPath, "utf8");
7546
7887
  const nonHollow = (JS_TEST_ASSERTION_PATTERN.test(contents) || CPP_TEST_ASSERTION_PATTERN.test(contents) || MULTI_ECOSYSTEM_TEST_ASSERTION_PATTERN.test(contents)) && !SUITE_LEVEL_SKIP_PATTERN.test(contents);
7547
7888
  if (nonHollow) {
7548
7889
  nonHollowCount += 1;
@@ -7563,7 +7904,7 @@ function countPatternMatches(repoPath, files, pattern) {
7563
7904
  for (const file of files) {
7564
7905
  const fullPath = join(repoPath, file);
7565
7906
  if (!isRegularFile(fullPath)) continue;
7566
- count += [...readFileSync(fullPath, "utf8").matchAll(matcher)].length;
7907
+ count += [...readRepoText(fullPath, "utf8").matchAll(matcher)].length;
7567
7908
  }
7568
7909
  return count;
7569
7910
  }
@@ -7571,7 +7912,7 @@ function readCoveragePercent(repoPath, files) {
7571
7912
  for (const file of files) {
7572
7913
  const fullPath = join(repoPath, file);
7573
7914
  if (!isRegularFile(fullPath)) continue;
7574
- const contents = readFileSync(fullPath, "utf8");
7915
+ const contents = readRepoText(fullPath, "utf8");
7575
7916
  const summaryPercent = parseCoverageSummaryPercent(contents);
7576
7917
  if (summaryPercent !== null) return summaryPercent;
7577
7918
  const thresholdPercent = parseCoverageThresholdPercent(contents);
@@ -7661,7 +8002,7 @@ function readDependencySpecs(repoPath, manifests) {
7661
8002
  }
7662
8003
  function readGoModDependencySpecs(path) {
7663
8004
  const specs = [];
7664
- const contents = readFileSync(path, "utf8");
8005
+ const contents = readRepoText(path, "utf8");
7665
8006
  const blockPattern = /require\s*\(([^)]+)\)/gs;
7666
8007
  const singlePattern = /^require\s+(\S+)\s+(\S+)/gm;
7667
8008
  for (const blockMatch of contents.matchAll(blockPattern)) {
@@ -7682,7 +8023,7 @@ function readGoModDependencySpecs(path) {
7682
8023
  }
7683
8024
  function readCargoTomlDependencySpecs(path) {
7684
8025
  const specs = [];
7685
- const contents = readFileSync(path, "utf8");
8026
+ const contents = readRepoText(path, "utf8");
7686
8027
  const depSectionPattern = /^\[(?:dependencies|dev-dependencies|build-dependencies)\]([\s\S]*?)(?=^\[|(?![\s\S]))/gm;
7687
8028
  for (const sectionMatch of contents.matchAll(depSectionPattern)) {
7688
8029
  const section = sectionMatch[1] ?? "";
@@ -7704,7 +8045,7 @@ function readCargoTomlDependencySpecs(path) {
7704
8045
  }
7705
8046
  function readPyprojectDependencySpecs(path) {
7706
8047
  const specs = [];
7707
- const contents = readFileSync(path, "utf8");
8048
+ const contents = readRepoText(path, "utf8");
7708
8049
  const poetrySectionPattern = /^\[tool\.poetry\.dependencies\]([\s\S]*?)(?=^\[|(?![\s\S]))/gm;
7709
8050
  for (const sectionMatch of contents.matchAll(poetrySectionPattern)) {
7710
8051
  const section = sectionMatch[1] ?? "";
@@ -7738,7 +8079,7 @@ function readPyprojectDependencySpecs(path) {
7738
8079
  }
7739
8080
  function readPipfileDependencySpecs(path) {
7740
8081
  const specs = [];
7741
- const contents = readFileSync(path, "utf8");
8082
+ const contents = readRepoText(path, "utf8");
7742
8083
  const sectionPattern = /^\[(?:packages|dev-packages)\]([\s\S]*?)(?=^\[|$(?![\s\S]))/gm;
7743
8084
  for (const sectionMatch of contents.matchAll(sectionPattern)) {
7744
8085
  for (const line of (sectionMatch[1] ?? "").split("\n")) {
@@ -7751,7 +8092,7 @@ function readPipfileDependencySpecs(path) {
7751
8092
  return specs;
7752
8093
  }
7753
8094
  function readVcpkgJsonDependencySpecs(path) {
7754
- const parsed = parseJsonObject(readFileSync(path, "utf8"));
8095
+ const parsed = parseJsonObject(readRepoText(path, "utf8"));
7755
8096
  if (!parsed) return [];
7756
8097
  const rawDeps = Array.isArray(parsed.dependencies) ? parsed.dependencies : [];
7757
8098
  const specs = [];
@@ -7767,7 +8108,7 @@ function readVcpkgJsonDependencySpecs(path) {
7767
8108
  }
7768
8109
  function readConanfileTxtDependencySpecs(path) {
7769
8110
  const specs = [];
7770
- const contents = readFileSync(path, "utf8");
8111
+ const contents = readRepoText(path, "utf8");
7771
8112
  const requiresMatch = /^\[requires\]([\s\S]*?)(?=^\[|$(?![\s\S]))/m.exec(contents);
7772
8113
  if (!requiresMatch) return specs;
7773
8114
  for (const line of (requiresMatch[1] ?? "").split("\n")) {
@@ -7780,7 +8121,7 @@ function readConanfileTxtDependencySpecs(path) {
7780
8121
  }
7781
8122
  function readConanfilePyDependencySpecs(path) {
7782
8123
  const specs = [];
7783
- const contents = readFileSync(path, "utf8");
8124
+ const contents = readRepoText(path, "utf8");
7784
8125
  for (const match of contents.matchAll(/self\.requires\s*\(\s*["']([\w.\-]+)\/([\w.\-]+)["']/g)) {
7785
8126
  specs.push({ name: match[1] ?? "", version: match[2] ?? "" });
7786
8127
  }
@@ -7794,7 +8135,7 @@ function readConanfilePyDependencySpecs(path) {
7794
8135
  }
7795
8136
  function readCMakeDependencySpecs(path) {
7796
8137
  const specs = [];
7797
- const contents = readFileSync(path, "utf8");
8138
+ const contents = readRepoText(path, "utf8");
7798
8139
  for (const match of contents.matchAll(/find_package\s*\(\s*([\w]+)(?:\s+([\d.]+))?/gi)) {
7799
8140
  const name = match[1];
7800
8141
  if (!name) continue;
@@ -7809,7 +8150,7 @@ function readCMakeDependencySpecs(path) {
7809
8150
  return specs;
7810
8151
  }
7811
8152
  function readPackageJsonDependencySpecs(packageJsonPath) {
7812
- const parsed = parseJsonObject(readFileSync(packageJsonPath, "utf8"));
8153
+ const parsed = parseJsonObject(readRepoText(packageJsonPath, "utf8"));
7813
8154
  if (!parsed) return [];
7814
8155
  const optionalPeerNames = /* @__PURE__ */ new Set();
7815
8156
  const peerDependenciesMeta = parsed.peerDependenciesMeta;
@@ -7836,7 +8177,7 @@ function readPackageJsonDependencySpecs(packageJsonPath) {
7836
8177
  return specs;
7837
8178
  }
7838
8179
  function readRequirementsDependencySpecs(path) {
7839
- return readFileSync(path, "utf8").split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).map((line) => {
8180
+ return readRepoText(path, "utf8").split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).map((line) => {
7840
8181
  const [name = line, version = ""] = line.split(/[<>=~!]=?/, 2);
7841
8182
  return { name: name.trim(), version: line.includes("==") ? version.trim() : line };
7842
8183
  });
@@ -7854,7 +8195,7 @@ function isPinnedDependencyVersion(version) {
7854
8195
  function workflowTargetsDefaultBranch(repoPath, file) {
7855
8196
  const fullPath = join(repoPath, file);
7856
8197
  if (!isRegularFile(fullPath)) return false;
7857
- const contents = readFileSync(fullPath, "utf8").toLowerCase();
8198
+ const contents = readRepoText(fullPath, "utf8").toLowerCase();
7858
8199
  return /\bpull_request\b/.test(contents) || /\bbranches:\s*\[[^\]]*\b(main|master)\b[^\]]*\]/.test(contents) || /\bbranches:\s*\n\s*-\s*(main|master)\b/.test(contents);
7859
8200
  }
7860
8201
  function isPrMergeCommit(commit) {
@@ -8008,7 +8349,7 @@ function deriveRlsPolicyScopeInventory(repoPath, repoFiles) {
8008
8349
  for (const file of candidatePolicyFiles) {
8009
8350
  let content;
8010
8351
  try {
8011
- content = readFileSync(join(repoPath, file), "utf8");
8352
+ content = readRepoText(join(repoPath, file), "utf8");
8012
8353
  } catch {
8013
8354
  continue;
8014
8355
  }
@@ -8112,7 +8453,7 @@ function findClaimRealityReconciliationArtifacts(repoPath, repoFiles) {
8112
8453
  function isAuthenticatedClaimRealityArtifact(repoPath, file) {
8113
8454
  const fullPath = join(repoPath, file);
8114
8455
  if (!isRegularFile(fullPath)) return false;
8115
- const contents = readFileSync(fullPath, "utf8");
8456
+ const contents = readRepoText(fullPath, "utf8");
8116
8457
  if (/\.json$/i.test(file)) {
8117
8458
  return parseJsonObject(contents)?.artifactKind === "claim_reality_reconciliation";
8118
8459
  }
@@ -8151,7 +8492,7 @@ function isCoverageConfig(repoPath, file) {
8151
8492
  function packageJsonHasCoverageTooling(repoPath, file) {
8152
8493
  const fullPath = join(repoPath, file);
8153
8494
  if (!isRegularFile(fullPath)) return false;
8154
- const parsed = parseJsonObject(readFileSync(fullPath, "utf8"));
8495
+ const parsed = parseJsonObject(readRepoText(fullPath, "utf8"));
8155
8496
  if (!parsed) return false;
8156
8497
  const scripts = parsed.scripts;
8157
8498
  if (typeof scripts === "object" && scripts !== null && !Array.isArray(scripts)) {
@@ -8625,7 +8966,7 @@ function findRuntimeContainerEntrypointFile(repoPath, repoFiles) {
8625
8966
  return repoFiles.filter((file) => /(^|\/)Dockerfile$/i.test(file) && isAuthoredProductionPath(file)).find((file) => {
8626
8967
  const fullPath = join(repoPath, file);
8627
8968
  if (!isRegularFile(fullPath)) return false;
8628
- const lines = readFileSync(fullPath, "utf8").split(/\r?\n/);
8969
+ const lines = readRepoText(fullPath, "utf8").split(/\r?\n/);
8629
8970
  const finalStageStart = lines.reduce(
8630
8971
  (last, line, index) => /^\s*FROM\b/i.test(line) ? index : last,
8631
8972
  0
@@ -8674,7 +9015,7 @@ function packageJsonHasAuditScript(repoPath, manifest) {
8674
9015
  function packageJsonHasHeavyTestDependency(repoPath, manifest) {
8675
9016
  const fullPath = join(repoPath, manifest);
8676
9017
  if (!isRegularFile(fullPath)) return false;
8677
- const parsed = parseJsonObject(readFileSync(fullPath, "utf8"));
9018
+ const parsed = parseJsonObject(readRepoText(fullPath, "utf8"));
8678
9019
  if (!parsed) return false;
8679
9020
  const names = [
8680
9021
  ...Object.keys(isRecord(parsed.dependencies) ? parsed.dependencies : {}),
@@ -8685,7 +9026,7 @@ function packageJsonHasHeavyTestDependency(repoPath, manifest) {
8685
9026
  function packageJsonDescribesPackagedApplication(repoPath, manifest, repoFiles) {
8686
9027
  const fullPath = join(repoPath, manifest);
8687
9028
  if (!isRegularFile(fullPath)) return false;
8688
- const parsed = parseJsonObject(readFileSync(fullPath, "utf8"));
9029
+ const parsed = parseJsonObject(readRepoText(fullPath, "utf8"));
8689
9030
  if (!parsed) return false;
8690
9031
  const scripts = isRecord(parsed.scripts) ? Object.values(parsed.scripts) : [];
8691
9032
  const dependencies = {
@@ -8710,7 +9051,7 @@ function findTauriApplicationMetadata(repoPath, manifest, repoFiles) {
8710
9051
  if (!repoFiles.includes(file)) return false;
8711
9052
  const fullPath = join(repoPath, file);
8712
9053
  if (!isRegularFile(fullPath)) return false;
8713
- const config = parseJsonObject(readFileSync(fullPath, "utf8"));
9054
+ const config = parseJsonObject(readRepoText(fullPath, "utf8"));
8714
9055
  const packageMetadata = isRecord(config?.package) ? config.package : null;
8715
9056
  return isNonEmptyString(config?.productName) || isNonEmptyString(config?.identifier) || packageMetadata != null && isNonEmptyString(packageMetadata.productName);
8716
9057
  });
@@ -8757,12 +9098,12 @@ function isRegularFile(path) {
8757
9098
  function fileContains(repoPath, file, pattern) {
8758
9099
  const fullPath = join(repoPath, file);
8759
9100
  if (!isRegularFile(fullPath)) return false;
8760
- return pattern.test(readFileSync(fullPath, "utf8"));
9101
+ return pattern.test(readRepoText(fullPath, "utf8"));
8761
9102
  }
8762
9103
  function findCommittedSecretInFile(repoPath, file, useV36Detectors = false, useV39Detectors = false, useV47Detectors = false) {
8763
9104
  const fullPath = join(repoPath, file);
8764
9105
  if (!isRegularFile(fullPath)) return null;
8765
- const contents = readFileSync(fullPath, "utf8");
9106
+ const contents = readRepoText(fullPath, "utf8");
8766
9107
  const isTestCredentialConfiguration = useV39Detectors && isLikelyTestCredentialConfiguration(file, contents);
8767
9108
  const secretScanContents = prepareCredentialScanContents(
8768
9109
  contents,
@@ -8855,7 +9196,7 @@ function isLikelyTestCredentialConfiguration(file, contents) {
8855
9196
  function hasTemplateOnlyEnvContent(repoPath, file) {
8856
9197
  const fullPath = join(repoPath, file);
8857
9198
  if (!isRegularFile(fullPath)) return false;
8858
- const substantiveLines = readFileSync(fullPath, "utf8").split(/\r?\n/).filter((line) => line.trim().length > 0 && !/^\s*#/.test(line));
9199
+ const substantiveLines = readRepoText(fullPath, "utf8").split(/\r?\n/).filter((line) => line.trim().length > 0 && !/^\s*#/.test(line));
8859
9200
  if (substantiveLines.length === 0) return true;
8860
9201
  const assignments = substantiveLines.map(
8861
9202
  (line) => /^\s*(?:export\s+)?[A-Za-z_][A-Za-z0-9_]*\s*=\s*(.*?)\s*$/.exec(line)?.[1] ?? null
@@ -8870,7 +9211,7 @@ function hasTemplateOnlyEnvContent(repoPath, file) {
8870
9211
  function findSecretFingerprintsInFile(repoPath, file, useV36Detectors = false, useV39Detectors = false, useV47Detectors = false) {
8871
9212
  const fullPath = join(repoPath, file);
8872
9213
  if (!isRegularFile(fullPath)) return [];
8873
- const contents = readFileSync(fullPath, "utf8");
9214
+ const contents = readRepoText(fullPath, "utf8");
8874
9215
  const scanContents = prepareCredentialScanContents(
8875
9216
  contents,
8876
9217
  file,
@@ -9021,8 +9362,18 @@ function secretEvidenceLabel(prefix, match) {
9021
9362
  function isSecretIdentifierCharacter(characterCode) {
9022
9363
  return characterCode >= 48 && characterCode <= 57 || characterCode >= 65 && characterCode <= 90 || characterCode === 95 || characterCode >= 97 && characterCode <= 122;
9023
9364
  }
9024
- function collectHistorySecretEvidence(repoPath, excludeHead, currentSecretFingerprintsByPath, authoredProductionOnly = false, useV36Detectors = false, useV39Detectors = false, useV47Detectors = false) {
9025
- const historyEntries = readCredentialHistoryEntries(repoPath, useV36Detectors);
9365
+ var CONTROL_CHARACTER_HISTORY_PATH_LIMITATION = "Git history contained a credential-shaped pathname with control characters. Cejel omitted that path from historical blob inspection and continued with reduced history evidence.";
9366
+ function declareHistoryFailure(scanLimitations, reason, operation, maxBufferBytes) {
9367
+ scanLimitations.add(
9368
+ `${describeGitFailure(reason, operation, maxBufferBytes)} Cejel continued with reduced Git history evidence.`
9369
+ );
9370
+ }
9371
+ function collectHistorySecretEvidence(repoPath, excludeHead, currentSecretFingerprintsByPath, scanLimitations, authoredProductionOnly = false, useV36Detectors = false, useV39Detectors = false, useV47Detectors = false) {
9372
+ const historyEntries = readCredentialHistoryEntries(
9373
+ repoPath,
9374
+ scanLimitations,
9375
+ useV36Detectors
9376
+ );
9026
9377
  const head = excludeHead ? readGitHead(repoPath) : null;
9027
9378
  let scannedFiles = 0;
9028
9379
  let envPathEvidence = null;
@@ -9035,7 +9386,7 @@ function collectHistorySecretEvidence(repoPath, excludeHead, currentSecretFinger
9035
9386
  return { evidence: null, envPathEvidence, truncated: true };
9036
9387
  }
9037
9388
  scannedFiles += 1;
9038
- const contents = readGitBlob(repoPath, entry.commit, entry.path);
9389
+ const contents = readGitBlob(repoPath, entry.commit, entry.path, scanLimitations);
9039
9390
  if (!contents) continue;
9040
9391
  const scanContents = prepareCredentialScanContents(
9041
9392
  contents,
@@ -9078,7 +9429,7 @@ function collectHistorySecretEvidence(repoPath, excludeHead, currentSecretFinger
9078
9429
  }
9079
9430
  return { evidence: null, envPathEvidence, truncated: false };
9080
9431
  }
9081
- function readCredentialHistoryEntries(repoPath, useV36Detectors = false) {
9432
+ function readCredentialHistoryEntries(repoPath, scanLimitations, useV36Detectors = false) {
9082
9433
  const entries = [];
9083
9434
  const seen = /* @__PURE__ */ new Set();
9084
9435
  const appendEntry = (entry) => {
@@ -9087,14 +9438,12 @@ function readCredentialHistoryEntries(repoPath, useV36Detectors = false) {
9087
9438
  seen.add(key);
9088
9439
  entries.push(entry);
9089
9440
  };
9090
- try {
9091
- const commits = execFileSync("git", ["rev-list", "HEAD"], {
9092
- cwd: repoPath,
9093
- encoding: "utf8",
9094
- stdio: ["ignore", "pipe", "ignore"]
9095
- });
9096
- const output = execFileSync(
9097
- "git",
9441
+ const commitsResult = execGit(["rev-list", "HEAD"], { cwd: repoPath });
9442
+ if (!commitsResult.ok && !isExpectedGitAbsence(commitsResult)) {
9443
+ declareHistoryFailure(scanLimitations, commitsResult.reason, "history revision enumeration");
9444
+ }
9445
+ if (commitsResult.ok) {
9446
+ const historyResult = execGit(
9098
9447
  [
9099
9448
  "diff-tree",
9100
9449
  "--stdin",
@@ -9102,39 +9451,53 @@ function readCredentialHistoryEntries(repoPath, useV36Detectors = false) {
9102
9451
  "--name-only",
9103
9452
  "-r",
9104
9453
  "--diff-filter=AM",
9105
- "--pretty=format:commit:%H"
9454
+ "-z",
9455
+ "--pretty=format:commit:%H%x00"
9106
9456
  ],
9107
- {
9108
- cwd: repoPath,
9109
- encoding: "utf8",
9110
- input: commits,
9111
- stdio: ["pipe", "pipe", "ignore"]
9112
- }
9457
+ { cwd: repoPath, input: commitsResult.stdout }
9113
9458
  );
9114
- let currentCommit = null;
9115
- for (const line of output.split("\n")) {
9116
- if (line.startsWith("commit:")) {
9117
- currentCommit = line.slice("commit:".length);
9118
- continue;
9459
+ if (!historyResult.ok) {
9460
+ if (!isExpectedGitAbsence(historyResult)) {
9461
+ declareHistoryFailure(scanLimitations, historyResult.reason, "history tree enumeration");
9462
+ }
9463
+ } else {
9464
+ let currentCommit = null;
9465
+ for (const rawToken of historyResult.stdout.split("\0")) {
9466
+ const token = rawToken.startsWith("\n") ? rawToken.slice(1) : rawToken;
9467
+ if (token.startsWith("commit:")) {
9468
+ currentCommit = token.slice("commit:".length);
9469
+ continue;
9470
+ }
9471
+ const path = token;
9472
+ if (!currentCommit || !path) continue;
9473
+ if (/[\u0000-\u001f\u007f]/.test(path)) {
9474
+ scanLimitations.add(CONTROL_CHARACTER_HISTORY_PATH_LIMITATION);
9475
+ continue;
9476
+ }
9477
+ if (!isCredentialHistoryPath(path, useV36Detectors)) continue;
9478
+ appendEntry({ commit: currentCommit, path });
9119
9479
  }
9120
- const path = line.trim();
9121
- if (!currentCommit || !path) continue;
9122
- if (!isCredentialHistoryPath(path, useV36Detectors)) continue;
9123
- appendEntry({ commit: currentCommit, path });
9124
9480
  }
9125
- } catch {
9126
9481
  }
9127
- for (const entry of readDeletedCredentialHistoryEntries(repoPath, useV36Detectors)) {
9482
+ for (const entry of readDeletedCredentialHistoryEntries(
9483
+ repoPath,
9484
+ scanLimitations,
9485
+ useV36Detectors
9486
+ )) {
9128
9487
  appendEntry(entry);
9129
9488
  }
9130
9489
  return entries;
9131
9490
  }
9132
- function readDeletedCredentialHistoryEntries(repoPath, useV36Detectors = false) {
9133
- const deletedPaths = readDeletedCredentialHistoryPaths(repoPath, useV36Detectors);
9491
+ function readDeletedCredentialHistoryEntries(repoPath, scanLimitations, useV36Detectors = false) {
9492
+ const deletedPaths = readDeletedCredentialHistoryPaths(
9493
+ repoPath,
9494
+ scanLimitations,
9495
+ useV36Detectors
9496
+ );
9134
9497
  const entries = [];
9135
9498
  const seen = /* @__PURE__ */ new Set();
9136
9499
  for (const path of deletedPaths) {
9137
- for (const commit of readHistoryCommitsForPath(repoPath, path)) {
9500
+ for (const commit of readHistoryCommitsForPath(repoPath, path, scanLimitations)) {
9138
9501
  const key = `${commit}:${path}`;
9139
9502
  if (seen.has(key)) continue;
9140
9503
  seen.add(key);
@@ -9143,47 +9506,51 @@ function readDeletedCredentialHistoryEntries(repoPath, useV36Detectors = false)
9143
9506
  }
9144
9507
  return entries;
9145
9508
  }
9146
- function readDeletedCredentialHistoryPaths(repoPath, useV36Detectors = false) {
9147
- try {
9148
- const output = execFileSync(
9149
- "git",
9150
- ["log", "HEAD", "--diff-filter=D", "--name-status", "--format=commit:%H"],
9151
- {
9152
- cwd: repoPath,
9153
- encoding: "utf8",
9154
- stdio: ["ignore", "pipe", "ignore"]
9155
- }
9156
- );
9157
- const paths = [];
9158
- const seen = /* @__PURE__ */ new Set();
9159
- for (const line of output.split("\n")) {
9160
- if (!line.startsWith("D ")) continue;
9161
- const path = line.slice(2).trim();
9162
- if (!path || seen.has(path)) continue;
9163
- if (!isCredentialHistoryPath(path, useV36Detectors)) continue;
9164
- seen.add(path);
9165
- paths.push(path);
9166
- }
9167
- return paths;
9168
- } catch {
9509
+ function readDeletedCredentialHistoryPaths(repoPath, scanLimitations, useV36Detectors = false) {
9510
+ const historyResult = execGit(
9511
+ [
9512
+ "log",
9513
+ "--no-show-signature",
9514
+ "HEAD",
9515
+ "--diff-filter=D",
9516
+ "--name-only",
9517
+ "-z",
9518
+ "--format=commit:%H%x00"
9519
+ ],
9520
+ { cwd: repoPath }
9521
+ );
9522
+ if (!historyResult.ok) {
9523
+ if (isExpectedGitAbsence(historyResult)) return [];
9524
+ declareHistoryFailure(scanLimitations, historyResult.reason, "deleted-path history scan");
9169
9525
  return [];
9170
9526
  }
9527
+ const paths = [];
9528
+ const seen = /* @__PURE__ */ new Set();
9529
+ for (const rawToken of historyResult.stdout.split("\0")) {
9530
+ const path = rawToken.startsWith("\n") ? rawToken.slice(1) : rawToken;
9531
+ if (path.startsWith("commit:") || !path) continue;
9532
+ if (/[\u0000-\u001f\u007f]/.test(path)) {
9533
+ scanLimitations.add(CONTROL_CHARACTER_HISTORY_PATH_LIMITATION);
9534
+ continue;
9535
+ }
9536
+ if (!path || seen.has(path)) continue;
9537
+ if (!isCredentialHistoryPath(path, useV36Detectors)) continue;
9538
+ seen.add(path);
9539
+ paths.push(path);
9540
+ }
9541
+ return paths;
9171
9542
  }
9172
- function readHistoryCommitsForPath(repoPath, path) {
9173
- try {
9174
- const output = execFileSync(
9175
- "git",
9176
- ["log", "HEAD", "--diff-filter=AM", "--format=%H", "--", path],
9177
- {
9178
- cwd: repoPath,
9179
- encoding: "utf8",
9180
- stdio: ["ignore", "pipe", "ignore"]
9181
- }
9182
- );
9183
- return output.split("\n").map((line) => line.trim()).filter((line) => /^[0-9a-f]{40}$/i.test(line));
9184
- } catch {
9543
+ function readHistoryCommitsForPath(repoPath, path, scanLimitations) {
9544
+ const historyResult = execGit(
9545
+ ["log", "--no-show-signature", "HEAD", "--diff-filter=AM", "--format=%H", "--", path],
9546
+ { cwd: repoPath }
9547
+ );
9548
+ if (!historyResult.ok) {
9549
+ if (isExpectedGitAbsence(historyResult)) return [];
9550
+ declareHistoryFailure(scanLimitations, historyResult.reason, "path history scan");
9185
9551
  return [];
9186
9552
  }
9553
+ return historyResult.stdout.split("\n").map((line) => line.trim()).filter((line) => /^[0-9a-f]{40}$/i.test(line));
9187
9554
  }
9188
9555
  function isCredentialHistoryPath(path, _useV36Detectors = false) {
9189
9556
  if (isHardExcludedPath(path)) return false;
@@ -9200,17 +9567,23 @@ function isEnvHistoryPath(path, useV36Detectors = false, useV47Detectors = false
9200
9567
  const basename3 = path.toLowerCase().split("/").at(-1) ?? path.toLowerCase();
9201
9568
  return (useV36Detectors ? /^\.env(?:\.|$)/ : /\.env(?:\.|$)/).test(basename3);
9202
9569
  }
9203
- function readGitBlob(repoPath, commit, path) {
9204
- try {
9205
- return execFileSync("git", ["show", `${commit}:${path}`], {
9206
- cwd: repoPath,
9207
- encoding: "utf8",
9208
- maxBuffer: 512e3,
9209
- stdio: ["ignore", "pipe", "ignore"]
9210
- });
9211
- } catch {
9570
+ function readGitBlob(repoPath, commit, path, scanLimitations) {
9571
+ const maxBufferBytes = 512e3;
9572
+ const blobResult = execGit(["show", `${commit}:${path}`], {
9573
+ cwd: repoPath,
9574
+ maxBufferBytes
9575
+ });
9576
+ if (!blobResult.ok) {
9577
+ if (isExpectedGitAbsence(blobResult)) return null;
9578
+ declareHistoryFailure(
9579
+ scanLimitations,
9580
+ blobResult.reason,
9581
+ "historical blob read",
9582
+ maxBufferBytes
9583
+ );
9212
9584
  return null;
9213
9585
  }
9586
+ return blobResult.stdout;
9214
9587
  }
9215
9588
  var ALL_CAPS_SNAKE_PLACEHOLDER_PATTERN = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/;
9216
9589
  function isPlaceholderSecretValue(value) {
@@ -9240,7 +9613,7 @@ function hasSuspiciousDependencies(repoPath, manifest, useV47Detectors = false)
9240
9613
  if (!manifest.endsWith("package.json")) return false;
9241
9614
  const fullPath = join(repoPath, manifest);
9242
9615
  if (!isRegularFile(fullPath)) return false;
9243
- const parsed = parseJsonObject(readFileSync(fullPath, "utf8"));
9616
+ const parsed = parseJsonObject(readRepoText(fullPath, "utf8"));
9244
9617
  if (!parsed) return false;
9245
9618
  const dependencies = isRecord(parsed.dependencies) ? parsed.dependencies : {};
9246
9619
  const devDependencies = isRecord(parsed.devDependencies) ? parsed.devDependencies : {};
@@ -9256,24 +9629,26 @@ function hasSuspiciousDependencies(repoPath, manifest, useV47Detectors = false)
9256
9629
  return placeholderIdentity && placeholderMetadata;
9257
9630
  }
9258
9631
  function readRecentCommits(repoPath) {
9259
- try {
9260
- const output = execFileSync("git", ["log", "--max-count=12", "--format=%H%x00%s"], {
9261
- cwd: repoPath,
9262
- encoding: "utf8",
9263
- stdio: ["ignore", "pipe", "ignore"]
9264
- }).trim();
9265
- if (!output) return [];
9266
- return output.split("\n").map((line) => {
9267
- const [sha = "", subject = ""] = line.split("\0");
9268
- return { sha, subject };
9269
- }).filter((commit) => commit.sha.length >= 7);
9270
- } catch {
9271
- return [];
9632
+ const historyResult = execGit(
9633
+ ["log", "--no-show-signature", "--max-count=12", "--format=%H%x00%s"],
9634
+ {
9635
+ cwd: repoPath
9636
+ }
9637
+ );
9638
+ if (!historyResult.ok) {
9639
+ if (isExpectedGitAbsence(historyResult)) return [];
9640
+ throw new Error(describeGitFailure(historyResult.reason, "recent history scan"));
9272
9641
  }
9642
+ const output = historyResult.stdout.trim();
9643
+ if (!output) return [];
9644
+ return output.split("\n").map((line) => {
9645
+ const [sha = "", subject = ""] = line.split("\0");
9646
+ return { sha, subject };
9647
+ }).filter((commit) => commit.sha.length >= 7);
9273
9648
  }
9274
9649
  function readPackageScripts(packageJsonPath) {
9275
9650
  if (!isRegularFile(packageJsonPath)) return /* @__PURE__ */ new Map();
9276
- const parsed = parseJsonObject(readFileSync(packageJsonPath, "utf8"));
9651
+ const parsed = parseJsonObject(readRepoText(packageJsonPath, "utf8"));
9277
9652
  const scripts = parsed?.scripts;
9278
9653
  if (!isRecord(scripts)) return /* @__PURE__ */ new Map();
9279
9654
  return new Map(
@@ -9284,7 +9659,7 @@ function readPackageScripts(packageJsonPath) {
9284
9659
  }
9285
9660
  function evidenceForRelative(repoPath, path, kind, label) {
9286
9661
  const fullPath = join(repoPath, path);
9287
- const contents = isRegularFile(fullPath) ? readFileSync(fullPath, "utf8") : "";
9662
+ const contents = isRegularFile(fullPath) ? readRepoText(fullPath, "utf8") : "";
9288
9663
  return {
9289
9664
  kind,
9290
9665
  label,
@@ -9297,7 +9672,7 @@ function evidenceForRelative(repoPath, path, kind, label) {
9297
9672
  }
9298
9673
  function evidenceForRelativeAtLine(repoPath, path, kind, label, line) {
9299
9674
  const fullPath = join(repoPath, path);
9300
- const contents = isRegularFile(fullPath) ? readFileSync(fullPath, "utf8") : "";
9675
+ const contents = isRegularFile(fullPath) ? readRepoText(fullPath, "utf8") : "";
9301
9676
  return {
9302
9677
  kind,
9303
9678
  label,
@@ -9324,43 +9699,46 @@ function firstMeaningfulLine(contents) {
9324
9699
  return index === -1 ? 1 : index + 1;
9325
9700
  }
9326
9701
  function readGitHead(repoPath) {
9327
- try {
9328
- return execFileSync("git", ["rev-parse", "HEAD"], {
9329
- cwd: repoPath,
9330
- encoding: "utf8",
9331
- stdio: ["ignore", "pipe", "ignore"]
9332
- }).trim();
9333
- } catch {
9334
- return null;
9702
+ const headResult = execGit(["rev-parse", "HEAD"], { cwd: repoPath });
9703
+ if (!headResult.ok) {
9704
+ if (isExpectedGitAbsence(headResult)) return null;
9705
+ throw new Error(describeGitFailure(headResult.reason, "HEAD resolution"));
9335
9706
  }
9707
+ return headResult.stdout.trim() || null;
9336
9708
  }
9337
- function hasEnvPathInGitHistory(repoPath) {
9338
- try {
9339
- const output = execFileSync(
9340
- "git",
9341
- ["log", "HEAD", "--diff-filter=AM", "--name-only", "--format="],
9342
- {
9343
- cwd: repoPath,
9344
- encoding: "utf8",
9345
- stdio: ["ignore", "pipe", "ignore"],
9346
- maxBuffer: 512e3
9347
- }
9348
- ).trim();
9349
- if (!output) return false;
9350
- return output.split("\n").some((p) => /(?:^|\/)\.env(?:\.|$)/i.test(p.trim()));
9351
- } catch {
9709
+ function hasEnvPathInGitHistory(repoPath, scanLimitations) {
9710
+ const historyResult = execGit(
9711
+ ["log", "--no-show-signature", "HEAD", "--diff-filter=AM", "--name-only", "-z", "--format="],
9712
+ { cwd: repoPath }
9713
+ );
9714
+ if (!historyResult.ok) {
9715
+ if (isExpectedGitAbsence(historyResult)) return false;
9716
+ declareHistoryFailure(scanLimitations, historyResult.reason, "environment-file history scan");
9352
9717
  return false;
9353
9718
  }
9719
+ const output = historyResult.stdout;
9720
+ if (!output) return false;
9721
+ return output.split("\0").some((rawPath) => {
9722
+ const path = rawPath.startsWith("\n") ? rawPath.slice(1) : rawPath;
9723
+ if (/[\u0000-\u001f\u007f]/.test(path)) {
9724
+ scanLimitations.add(CONTROL_CHARACTER_HISTORY_PATH_LIMITATION);
9725
+ return false;
9726
+ }
9727
+ return /(?:^|\/)\.env(?:\.|$)/i.test(path);
9728
+ });
9354
9729
  }
9355
9730
 
9356
9731
  // src/witan/public-scan.ts
9732
+ init_esm_shims();
9357
9733
  import { resolve as resolve2 } from "path";
9358
9734
 
9359
9735
  // src/witan/ingest.ts
9360
- import { readFileSync as readFileSync5, readdirSync as readdirSync2 } from "fs";
9736
+ init_esm_shims();
9737
+ import { readFileSync as readFileSync4, readdirSync as readdirSync2 } from "fs";
9361
9738
  import { join as join2 } from "path";
9362
9739
 
9363
9740
  // src/witan/finding-limits.ts
9741
+ init_esm_shims();
9364
9742
  var RULE_ID_MAX = 200;
9365
9743
  var MESSAGE_MAX = 500;
9366
9744
  var LOCATION_MAX = 700;
@@ -9379,7 +9757,8 @@ function clampFinding(finding) {
9379
9757
  }
9380
9758
 
9381
9759
  // src/witan/generic-adapter.ts
9382
- import { readFileSync as readFileSync2 } from "fs";
9760
+ init_esm_shims();
9761
+ import { readFileSync } from "fs";
9383
9762
  var VALID_DIMENSIONS = /* @__PURE__ */ new Set([
9384
9763
  "A1",
9385
9764
  "A2",
@@ -9426,7 +9805,8 @@ function parseGenericJson(raw) {
9426
9805
  }
9427
9806
 
9428
9807
  // src/witan/sarif-adapter.ts
9429
- import { readFileSync as readFileSync3 } from "fs";
9808
+ init_esm_shims();
9809
+ import { readFileSync as readFileSync2 } from "fs";
9430
9810
  var DIMENSION_RULES = [
9431
9811
  // A2 — injection / secrets / auth (high weight)
9432
9812
  {
@@ -9571,7 +9951,8 @@ function parseSarifJson(raw, extraDimensionRules) {
9571
9951
  }
9572
9952
 
9573
9953
  // src/witan/scorecard-adapter.ts
9574
- import { readFileSync as readFileSync4 } from "fs";
9954
+ init_esm_shims();
9955
+ import { readFileSync as readFileSync3 } from "fs";
9575
9956
  var CHECK_RULES = {
9576
9957
  // B2 — PR outcome traceability
9577
9958
  "Branch-Protection": { dimension: "B2", weight: 0.8 },
@@ -9680,10 +10061,18 @@ function rawToolName(raw) {
9680
10061
  if (typeof obj.tool === "string" && obj.tool.length > 0) return obj.tool;
9681
10062
  return "scorecard";
9682
10063
  }
10064
+ var MAX_INGEST_SOURCE_LENGTH = 120;
10065
+ function sanitizeIngestSource(source) {
10066
+ const withoutControls = source.replace(/[\p{Cc}\p{Cf}]/gu, "").trim();
10067
+ const safeSource = withoutControls.length > 0 ? withoutControls : "unknown";
10068
+ const codePoints = Array.from(safeSource);
10069
+ if (codePoints.length <= MAX_INGEST_SOURCE_LENGTH) return safeSource;
10070
+ return `${codePoints.slice(0, MAX_INGEST_SOURCE_LENGTH - 3).join("")}...`;
10071
+ }
9683
10072
  function parseIngestFile(filePath, options = {}) {
9684
10073
  let raw;
9685
10074
  try {
9686
- raw = JSON.parse(stripBom(readFileSync5(filePath, "utf8")));
10075
+ raw = JSON.parse(stripBom(readFileSync4(filePath, "utf8")));
9687
10076
  } catch (error) {
9688
10077
  const message = error instanceof Error ? error.message : String(error);
9689
10078
  throw new Error(`Cejel: could not parse ingest file as JSON: ${filePath} (${message})`);
@@ -9707,6 +10096,8 @@ function parseIngestFile(filePath, options = {}) {
9707
10096
  }
9708
10097
  return signals.map((signal) => ({
9709
10098
  ...signal,
10099
+ source: sanitizeIngestSource(signal.source),
10100
+ provenance: options.provenance ?? "operator_supplied",
9710
10101
  findings: signal.findings.map(clampFinding)
9711
10102
  }));
9712
10103
  }
@@ -9715,7 +10106,7 @@ function expandIngestPattern(pattern) {
9715
10106
  const lastSlash = pattern.lastIndexOf("/");
9716
10107
  const dir = lastSlash === -1 ? "." : pattern.slice(0, lastSlash);
9717
10108
  const filePattern = lastSlash === -1 ? pattern : pattern.slice(lastSlash + 1);
9718
- const regexSource = filePattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
10109
+ const regexSource = filePattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
9719
10110
  const regex = new RegExp(`^${regexSource}$`);
9720
10111
  let entries;
9721
10112
  try {
@@ -9736,7 +10127,11 @@ function discoverIngestInputs(repoPath) {
9736
10127
  return entries.filter((name) => /\.(sarif|json)$/i.test(name)).sort().map((name) => join2(inputsDir, name));
9737
10128
  }
9738
10129
 
10130
+ // src/witan/scoring.ts
10131
+ init_esm_shims();
10132
+
9739
10133
  // src/witan/rubric.ts
10134
+ init_esm_shims();
9740
10135
  var WITAN_RUBRIC = [
9741
10136
  {
9742
10137
  id: "A1",
@@ -9803,6 +10198,9 @@ var MAX_SIGNAL_ADJUSTMENT = 0.8;
9803
10198
  function createWitanReport(input, inputSignals, rubric = WITAN_RUBRIC) {
9804
10199
  const parsedInput = WitanReportInputSchema.parse(input);
9805
10200
  const signalsByCriterion = mergeSignalsByCriterion(parsedInput.signals);
10201
+ const readFailureAbstentions = new Set(
10202
+ [...signalsByCriterion.values()].filter((signal) => signal.insufficientData === true).map((signal) => signal.criterionId)
10203
+ );
9806
10204
  const hasInputSignals = inputSignals != null && inputSignals.length > 0;
9807
10205
  const inputSignalsByDimension = /* @__PURE__ */ new Map();
9808
10206
  if (hasInputSignals && inputSignals) {
@@ -9866,7 +10264,8 @@ function createWitanReport(input, inputSignals, rubric = WITAN_RUBRIC) {
9866
10264
  const categoryScoreMap = {};
9867
10265
  for (const category of categoryOrder) {
9868
10266
  categoryScoreMap[category] = averageScore(
9869
- criteria.filter((criterion) => criterion.category === category)
10267
+ criteria.filter((criterion) => criterion.category === category),
10268
+ readFailureAbstentions
9870
10269
  );
9871
10270
  }
9872
10271
  const firstCategory = categoryOrder[0];
@@ -9889,7 +10288,6 @@ function createWitanReport(input, inputSignals, rubric = WITAN_RUBRIC) {
9889
10288
  productSlug: parsedInput.productSlug,
9890
10289
  productDisplayName: parsedInput.productDisplayName,
9891
10290
  repo: parsedInput.repo,
9892
- generatedAt: parsedInput.generatedAt,
9893
10291
  rubricVersion: parsedInput.rubricVersion,
9894
10292
  verdict: abstained ? "insufficient_source" : witanVerdictForScore(overallScore),
9895
10293
  codeTrustScore: abstained ? null : codeTrustScore,
@@ -9897,6 +10295,8 @@ function createWitanReport(input, inputSignals, rubric = WITAN_RUBRIC) {
9897
10295
  overallScore: abstained ? null : overallScore,
9898
10296
  criteria,
9899
10297
  ...consumedSignals.length > 0 ? { consumedSignals } : {},
10298
+ ...parsedInput.scanLimitations.length > 0 ? { scanLimitations: parsedInput.scanLimitations } : {},
10299
+ ...parsedInput.contentReadSummary ? { contentReadSummary: parsedInput.contentReadSummary } : {},
9900
10300
  // Only surface the full per-category map for rubrics with more than two buckets —
9901
10301
  // the default two-category rubric is fully represented by codeTrustScore/processTrustScore.
9902
10302
  ...!abstained && categoryOrder.length > 2 ? { categoryScores: categoryScoreMap } : {},
@@ -9922,6 +10322,7 @@ function applySignalAdjustment(nativeScore, dimension, signals) {
9922
10322
  totalRawAdj += signalAdj;
9923
10323
  summaries.push({
9924
10324
  source: signal.source,
10325
+ provenance: signal.provenance ?? "operator_supplied",
9925
10326
  dimension,
9926
10327
  findingCount: signal.findings.length,
9927
10328
  severityBreakdown: { critical: criticalCount, warning: warningCount, info: infoCount },
@@ -9987,8 +10388,9 @@ function mergeSignalsByCriterion(signals) {
9987
10388
  findings: [...existing.findings, ...signal.findings],
9988
10389
  metrics: [...existing.metrics ?? [], ...signal.metrics ?? []],
9989
10390
  notes: signal.notes ?? existing.notes,
9990
- // N/A wins on merge: if either signal is N/A, the criterion is N/A.
9991
- ...existing.notApplicable === true || signal.notApplicable === true ? { notApplicable: true } : {}
10391
+ // Read-failure abstention wins on merge: partial sibling evidence cannot turn an
10392
+ // incomplete criterion into a score. Otherwise N/A retains its historical precedence.
10393
+ ...existing.insufficientData === true || signal.insufficientData === true ? { insufficientData: true } : existing.notApplicable === true || signal.notApplicable === true ? { notApplicable: true } : {}
9992
10394
  });
9993
10395
  }
9994
10396
  return merged;
@@ -10009,6 +10411,15 @@ function scoreCriterion(signal, rubricVersion) {
10009
10411
  metrics: []
10010
10412
  };
10011
10413
  }
10414
+ if (signal.insufficientData === true) {
10415
+ return {
10416
+ score: 0,
10417
+ status: unmeasuredStatus(rubricVersion),
10418
+ evidence: [],
10419
+ findings: [],
10420
+ metrics: []
10421
+ };
10422
+ }
10012
10423
  if (signal.notApplicable === true) {
10013
10424
  return {
10014
10425
  score: 0,
@@ -10384,9 +10795,9 @@ function ensureFindingsExplainStatus(criterionId, score, status, findings, evide
10384
10795
  }
10385
10796
  ];
10386
10797
  }
10387
- function averageScore(criteria) {
10798
+ function averageScore(criteria, readFailureAbstentions = /* @__PURE__ */ new Set()) {
10388
10799
  const applicable = criteria.filter(
10389
- (criterion) => criterion.status !== "not_applicable" && criterion.status !== "insufficient_data"
10800
+ (criterion) => criterion.status !== "not_applicable" && (criterion.status !== "insufficient_data" || readFailureAbstentions.has(criterion.id))
10390
10801
  );
10391
10802
  if (applicable.length === 0) return 0;
10392
10803
  const total = applicable.reduce((sum, criterion) => sum + criterion.score, 0);
@@ -10421,7 +10832,7 @@ function scoreRepoWithPublicCejel(options) {
10421
10832
  }
10422
10833
  function resolvePublicIngestSignals(options) {
10423
10834
  const seen = /* @__PURE__ */ new Set();
10424
- const files = [];
10835
+ const explicitFiles = [];
10425
10836
  for (const pattern of options.ingestPatterns ?? []) {
10426
10837
  const matches = expandIngestPattern(pattern);
10427
10838
  if (matches.length === 0 && options.warnOnEmptyIngestMatch) {
@@ -10432,20 +10843,28 @@ function resolvePublicIngestSignals(options) {
10432
10843
  const resolved = resolve2(match);
10433
10844
  if (seen.has(resolved)) continue;
10434
10845
  seen.add(resolved);
10435
- files.push(match);
10846
+ explicitFiles.push(match);
10436
10847
  }
10437
10848
  }
10849
+ const signals = explicitFiles.flatMap(
10850
+ (file) => parseIngestFile(file, { provenance: "operator_supplied" })
10851
+ );
10852
+ if (!options.autoDiscoverIngest) return signals;
10438
10853
  for (const discovered of discoverIngestInputs(options.repoPath)) {
10439
10854
  const resolved = resolve2(discovered);
10440
10855
  if (seen.has(resolved)) continue;
10441
10856
  seen.add(resolved);
10442
- files.push(discovered);
10857
+ signals.push(...parseIngestFile(discovered, { provenance: "auto_discovered" }));
10443
10858
  }
10444
- return files.flatMap((file) => parseIngestFile(file));
10859
+ return signals;
10445
10860
  }
10446
10861
 
10862
+ // src/scan.ts
10863
+ init_esm_shims();
10864
+
10447
10865
  // src/product-identity.ts
10448
- import { existsSync, readFileSync as readFileSync6 } from "fs";
10866
+ init_esm_shims();
10867
+ import { existsSync, readFileSync as readFileSync5 } from "fs";
10449
10868
  import { basename as basename2, join as join3 } from "path";
10450
10869
  var SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$/;
10451
10870
  var FALLBACK_SLUG = "repo";
@@ -10462,7 +10881,7 @@ function deriveProductIdentity(repoPath) {
10462
10881
  function readPackageName(packageJsonPath) {
10463
10882
  if (!existsSync(packageJsonPath)) return void 0;
10464
10883
  try {
10465
- const parsed = JSON.parse(readFileSync6(packageJsonPath, "utf8"));
10884
+ const parsed = JSON.parse(readFileSync5(packageJsonPath, "utf8"));
10466
10885
  if (typeof parsed === "object" && parsed !== null && "name" in parsed) {
10467
10886
  const name = parsed.name;
10468
10887
  if (typeof name === "string" && name.trim().length > 0) return name;
@@ -10479,7 +10898,11 @@ function slugify(raw) {
10479
10898
  return FALLBACK_SLUG;
10480
10899
  }
10481
10900
 
10901
+ // src/summary.ts
10902
+ init_esm_shims();
10903
+
10482
10904
  // src/finding-presentation.ts
10905
+ init_esm_shims();
10483
10906
  var METRIC_DERIVED_FINDING_PATTERN2 = /^[A-Z]\d+ metric-derived score is (\d+\.\d)\/4\.0, in the (critical|warning) band — no single finding drove this; it reflects the combined metric weighting below\.$/;
10484
10907
  var REMEDIATION_BY_METRIC2 = {
10485
10908
  test_to_source_ratio: "add concrete regression tests for the implementation surface",
@@ -10591,13 +11014,14 @@ function buildWitanCliSummary(report) {
10591
11014
  const common = {
10592
11015
  productSlug: report.productSlug,
10593
11016
  productDisplayName: report.productDisplayName,
10594
- generatedAt: report.generatedAt,
10595
11017
  findingCount: allFindings.length,
10596
11018
  topFindings: sorted.slice(0, TOP_FINDINGS_LIMIT),
10597
11019
  contributingSources,
10598
11020
  externalSources,
10599
11021
  externalFindingCount: allExternalFindings.length,
10600
- topExternalFindings: allExternalFindings.slice(0, EXTERNAL_FINDINGS_DISPLAY_LIMIT)
11022
+ topExternalFindings: allExternalFindings.slice(0, EXTERNAL_FINDINGS_DISPLAY_LIMIT),
11023
+ scanLimitations: [...report.scanLimitations ?? []],
11024
+ ...report.contentReadSummary ? { contentReadSummary: report.contentReadSummary } : {}
10601
11025
  };
10602
11026
  if (report.verdict === "insufficient_source") {
10603
11027
  return {
@@ -10621,21 +11045,24 @@ function buildWitanCliSummary(report) {
10621
11045
  // src/scan.ts
10622
11046
  function runCejelScan(options) {
10623
11047
  const identity = deriveProductIdentity(options.repoPath);
11048
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
10624
11049
  const report = scoreRepoWithPublicCejel({
10625
11050
  productSlug: identity.productSlug,
10626
11051
  productDisplayName: options.productDisplayName ?? identity.productDisplayName,
10627
11052
  repoPath: options.repoPath,
11053
+ generatedAt,
10628
11054
  ingestPatterns: options.ingestPatterns,
11055
+ // Local CLI/MCP scans retain the documented convenience default: their operator controls
11056
+ // the repository. Direct public/leaderboard scans default this off because their targets
11057
+ // are third-party repositories and must opt in explicitly.
11058
+ autoDiscoverIngest: true,
10629
11059
  warnOnEmptyIngestMatch: options.warnOnEmptyIngestMatch
10630
11060
  });
10631
11061
  const summary = buildWitanCliSummary(report);
10632
- return { report, summary };
11062
+ return { report, summary, generatedAt };
10633
11063
  }
10634
11064
 
10635
11065
  export {
10636
- __commonJS,
10637
- __export,
10638
- __toESM,
10639
11066
  ZodOptional,
10640
11067
  ZodFirstPartyTypeKind,
10641
11068
  objectType,