@cejel/cejel 0.2.1 → 0.3.1

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",
@@ -4544,17 +4612,18 @@ function formatMetricValue(value) {
4544
4612
  }
4545
4613
 
4546
4614
  // src/witan/html.ts
4547
- function renderWitanHtmlReport(report) {
4615
+ function renderWitanHtmlReport(report, options = {}) {
4548
4616
  const codeCriteria = criteriaByCategory(report, "code_trust");
4549
4617
  const processCriteria = criteriaByCategory(report, "process_trust");
4550
4618
  const verifiedEvidence = report.criteria.flatMap(
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);
4626
+ const gitHistoryUnavailable = !report.repo.headSha;
4558
4627
  return `<!doctype html>
4559
4628
  <html lang="en">
4560
4629
  <head>
@@ -4575,10 +4644,11 @@ function renderWitanHtmlReport(report) {
4575
4644
  <p class="eyebrow">Trust Certificate</p>
4576
4645
  <h1>${escapeHtml(report.productDisplayName)}</h1>
4577
4646
  <dl class="meta">
4578
- <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>` : ""}
4579
4648
  <div><dt>Run</dt><dd>${escapeHtml(renderRepo(report))}</dd></div>
4649
+ <div><dt>CLI</dt><dd>${escapeHtml(options.cliVersion ? `Cejel ${options.cliVersion}` : "Not recorded")}</dd></div>
4580
4650
  <div><dt>Rubric</dt><dd>${escapeHtml(report.rubricVersion)}</dd></div>
4581
- ${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>` : ""}
4582
4652
  </dl>
4583
4653
  </section>
4584
4654
  <aside class="score-panel" aria-label="Summary trust score">
@@ -4596,9 +4666,18 @@ function renderWitanHtmlReport(report) {
4596
4666
  </div>
4597
4667
  </header>
4598
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
+
4599
4678
  <section class="trust-grid" aria-label="Rubric criteria">
4600
- ${renderCriterionColumn("Code trust", codeCriteria)}
4601
- ${renderCriterionColumn("Process trust", processCriteria)}
4679
+ ${renderCriterionColumn("Code trust", codeCriteria, gitHistoryUnavailable)}
4680
+ ${renderCriterionColumn("Process trust", processCriteria, gitHistoryUnavailable)}
4602
4681
  </section>
4603
4682
 
4604
4683
  <section class="evidence-grid" aria-label="Evidence and gaps">
@@ -4624,8 +4703,50 @@ function renderWitanHtmlReport(report) {
4624
4703
  </html>
4625
4704
  `;
4626
4705
  }
4627
- function renderContributingSources(signals) {
4628
- 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(", ")})`;
4629
4750
  }
4630
4751
  function renderExternalFindingItems(findings) {
4631
4752
  const shown = findings.slice(0, EXTERNAL_FINDINGS_DISPLAY_LIMIT);
@@ -4641,7 +4762,7 @@ function renderExternalFindingItems(findings) {
4641
4762
  function criteriaByCategory(report, category) {
4642
4763
  return report.criteria.filter((criterion) => criterion.category === category);
4643
4764
  }
4644
- function renderCriterionColumn(title, criteria) {
4765
+ function renderCriterionColumn(title, criteria, gitHistoryUnavailable) {
4645
4766
  const applicable = criteria.filter((criterion) => criterion.status !== "not_applicable");
4646
4767
  const notApplicable = criteria.filter((criterion) => criterion.status === "not_applicable");
4647
4768
  const notApplicableGroup = notApplicable.length > 0 ? `
@@ -4649,7 +4770,7 @@ function renderCriterionColumn(title, criteria) {
4649
4770
  return `<section class="criteria-column">
4650
4771
  <h2>${escapeHtml(title)}</h2>
4651
4772
  <div class="criteria-list">
4652
- ${applicable.map(renderCriterionCard).join("")}
4773
+ ${applicable.map((criterion) => renderCriterionCard(criterion, gitHistoryUnavailable)).join("")}
4653
4774
  </div>${notApplicableGroup}
4654
4775
  </section>`;
4655
4776
  }
@@ -4665,12 +4786,14 @@ function renderNotApplicableItem(criterion) {
4665
4786
  const reason = criterion.notes ?? "Not applicable to this repository.";
4666
4787
  return `<li><span class="na-id">${escapeHtml(criterion.id)}</span> ${escapeHtml(criterion.title)} \u2014 <span class="na-reason">${escapeHtml(reason)}</span></li>`;
4667
4788
  }
4668
- function renderCriterionCard(criterion) {
4789
+ function renderCriterionCard(criterion, gitHistoryUnavailable) {
4669
4790
  const evidence = [
4670
4791
  ...criterion.evidence.map(renderEvidencePointer),
4671
4792
  ...criterion.findings.map((finding) => renderFindingEvidence(criterion, finding))
4672
4793
  ];
4673
4794
  const metrics = criterion.metrics.map(renderMetric);
4795
+ const statusReconciliation = renderStatusReconciliation(criterion);
4796
+ const historyWarning = gitHistoryUnavailable && criterion.metrics.some((metric2) => metric2.name === "pr_merge_ratio") ? `<p class="scan-warning"><strong>Scan limitation:</strong> Git history was unavailable (for example, this may be a source tarball), so the recent PR merge ratio is not a measured repository outcome and its displayed zero may undercount ${escapeHtml(criterion.id)}. Re-scan a full Git clone for this signal.</p>` : "";
4674
4797
  return `<article class="criterion">
4675
4798
  <div class="criterion-top">
4676
4799
  <div>
@@ -4680,6 +4803,8 @@ function renderCriterionCard(criterion) {
4680
4803
  ${renderStatusChip(criterion.status)}
4681
4804
  </div>
4682
4805
  <div class="criterion-score">${criterion.status === "not_applicable" ? "N/A" : criterion.status === "insufficient_data" ? "No data" : formatScore(criterion.score)}</div>
4806
+ ${statusReconciliation}
4807
+ ${historyWarning}
4683
4808
  <ul class="criterion-metrics">
4684
4809
  ${metrics.length > 0 ? metrics.map((item) => `<li>${item}</li>`).join("") : "<li>No measured depth metrics supplied.</li>"}
4685
4810
  </ul>
@@ -4688,6 +4813,20 @@ function renderCriterionCard(criterion) {
4688
4813
  </ul>
4689
4814
  </article>`;
4690
4815
  }
4816
+ function renderStatusReconciliation(criterion) {
4817
+ if (criterion.status === "not_applicable" || criterion.status === "insufficient_data" || criterion.status === "unverified") {
4818
+ return "";
4819
+ }
4820
+ const numericBand = scoreBandForPresentation(criterion.score);
4821
+ if (numericBand === criterion.status) return "";
4822
+ return `<p class="status-explanation"><strong>Why the labels differ:</strong> the ${escapeHtml(criterion.status)} dimension band is calibrated from criterion evidence thresholds and findings, independently of the ${formatScore(criterion.score)}/4.0 weighted score (which falls in the ${numericBand} numeric band).</p>`;
4823
+ }
4824
+ function scoreBandForPresentation(score) {
4825
+ if (score >= 3.5) return "verified";
4826
+ if (score >= 2.5) return "info";
4827
+ if (score >= 1.5) return "warning";
4828
+ return "critical";
4829
+ }
4691
4830
  function renderMetric(metric2) {
4692
4831
  const unit = metric2.unit ? ` ${escapeHtml(metric2.unit)}` : "";
4693
4832
  if (metric2.kind === "saturating_count" && metric2.max !== void 0 && metric2.value > metric2.max) {
@@ -4844,6 +4983,8 @@ dd { margin: 0; color: var(--muted); overflow-wrap: anywhere; }
4844
4983
  .coverage-note { margin-top: 8px; color: var(--faint); font-family: var(--mono); font-size: 11px; }
4845
4984
  .source-counts { margin: 8px 0 0; padding-left: 18px; font-family: var(--mono); font-size: 12px; color: var(--muted); }
4846
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; }
4847
4988
  .external-findings-section { margin-top: 18px; }
4848
4989
  .external-note { margin: 0 0 14px; color: var(--muted); font-size: 13px; }
4849
4990
  .criteria-column, .evidence-section { border: 1px solid var(--line); border-radius: 8px; background: var(--surface); padding: 22px; }
@@ -4860,6 +5001,12 @@ h2 { font-family: var(--serif); font-size: 30px; font-weight: 400; letter-spacin
4860
5001
  .criterion-id { color: var(--periwinkle); font-family: var(--mono); font-size: 12px; margin-bottom: 4px; }
4861
5002
  h3 { font-size: 15px; line-height: 1.35; font-weight: 600; margin-bottom: 0; }
4862
5003
  .criterion-score { margin-top: 12px; color: var(--text); font-family: var(--mono); font-size: 24px; }
5004
+ .status-explanation, .scan-warning {
5005
+ margin: 10px 0 0; border-left: 3px solid var(--periwinkle); padding: 8px 10px;
5006
+ background: var(--periwinkle-weak); color: var(--muted); font-size: 12px; line-height: 1.5;
5007
+ }
5008
+ .scan-warning { border-left-color: var(--warn); background: var(--warn-weak); }
5009
+ .status-explanation strong, .scan-warning strong { color: var(--text); }
4863
5010
  .criterion-metrics { display: grid; gap: 7px; margin-top: 10px; padding: 0; list-style: none; font-size: 12px; }
4864
5011
  .criterion-metrics li { display: flex; justify-content: space-between; gap: 12px; border-bottom: 1px solid rgba(238, 244, 247, .08); padding-bottom: 6px; }
4865
5012
  .criterion-metrics strong { color: var(--text); font-weight: 650; }
@@ -4887,6 +5034,7 @@ a:hover { text-decoration: underline; text-underline-offset: 2px; }
4887
5034
  `;
4888
5035
 
4889
5036
  // src/witan/badge.ts
5037
+ init_esm_shims();
4890
5038
  var VERDICT_COLOR = {
4891
5039
  Verified: "brightgreen",
4892
5040
  Conditional: "yellow",
@@ -4904,24 +5052,26 @@ var VERDICT_SVG_FILL = {
4904
5052
  "Insufficient evidence": "#8c8c8c"
4905
5053
  };
4906
5054
  function badgeMessage(report, verdict) {
5055
+ if ((report.scanLimitations?.length ?? 0) > 0) return "unrated: limited evidence";
4907
5056
  if (isWitanNoMeasurementAbstention(report)) return "unrated: no measurable evidence";
4908
5057
  if (report.verdict === "insufficient_source") return "unrated: no source";
4909
5058
  return `${formatScore2(report.overallScore)}/4.0 ${verdict.toLowerCase()}`;
4910
5059
  }
4911
5060
  function renderWitanBadgeEndpoint(report) {
4912
5061
  const verdict = renderReportVerdict(report);
5062
+ const limited = (report.scanLimitations?.length ?? 0) > 0;
4913
5063
  return {
4914
5064
  schemaVersion: 1,
4915
5065
  label: "cejel trust",
4916
5066
  message: badgeMessage(report, verdict),
4917
- color: VERDICT_COLOR[verdict] ?? "lightgrey"
5067
+ color: limited ? "lightgrey" : VERDICT_COLOR[verdict] ?? "lightgrey"
4918
5068
  };
4919
5069
  }
4920
5070
  function renderWitanBadgeSvg(report) {
4921
5071
  const verdict = renderReportVerdict(report);
4922
5072
  const label = "cejel trust";
4923
5073
  const message = badgeMessage(report, verdict);
4924
- const fill = VERDICT_SVG_FILL[verdict] ?? "#8c8c8c";
5074
+ const fill = (report.scanLimitations?.length ?? 0) > 0 ? "#8c8c8c" : VERDICT_SVG_FILL[verdict] ?? "#8c8c8c";
4925
5075
  const charWidth = 6.5;
4926
5076
  const padding = 10;
4927
5077
  const labelWidth = Math.round(label.length * charWidth + padding);
@@ -4957,6 +5107,7 @@ function escapeXml(value) {
4957
5107
  }
4958
5108
 
4959
5109
  // src/witan/attestation.ts
5110
+ init_esm_shims();
4960
5111
  import { createHash } from "crypto";
4961
5112
  var ATTESTATION_LIMITATIONS = [
4962
5113
  "Cejel measures observable repository signals; it is not a security or compliance guarantee.",
@@ -4970,6 +5121,19 @@ function hashWitanReport(report) {
4970
5121
  }
4971
5122
  function createWitanAttestation(report, options) {
4972
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
+ );
4973
5137
  const outcome = report.verdict === "insufficient_source" ? {
4974
5138
  status: "abstained",
4975
5139
  reason: report.insufficientSourceReason
@@ -4990,7 +5154,7 @@ function createWitanAttestation(report, options) {
4990
5154
  predicateType: WITAN_ATTESTATION_PREDICATE_TYPE,
4991
5155
  predicate: {
4992
5156
  tool: { name: "cejel", version: options.toolVersion },
4993
- generatedAt: report.generatedAt,
5157
+ generatedAt: options.generatedAt,
4994
5158
  rubricVersion: report.rubricVersion,
4995
5159
  repository: {
4996
5160
  productSlug: report.productSlug,
@@ -5001,6 +5165,7 @@ function createWitanAttestation(report, options) {
5001
5165
  artifact: "report.json",
5002
5166
  sha256: reportSha256
5003
5167
  },
5168
+ externalSignalProvenance,
5004
5169
  outcome,
5005
5170
  assurance: {
5006
5171
  status: "unsigned",
@@ -5033,9 +5198,6 @@ function verifyWitanAttestationBinding(statement, report, options = {}) {
5033
5198
  if (parsed.data.predicate.rubricVersion !== report.rubricVersion) {
5034
5199
  errors.push("rubric version does not match report.json");
5035
5200
  }
5036
- if (parsed.data.predicate.generatedAt !== report.generatedAt) {
5037
- errors.push("generated timestamp does not match report.json");
5038
- }
5039
5201
  if (parsed.data.predicate.repository.headSha !== report.repo.headSha) {
5040
5202
  errors.push("repository revision does not match report.json");
5041
5203
  }
@@ -5045,6 +5207,22 @@ function verifyWitanAttestationBinding(statement, report, options = {}) {
5045
5207
  if (parsed.data.predicate.repository.url !== report.repo.url) {
5046
5208
  errors.push("repository URL does not match report.json");
5047
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
+ }
5048
5226
  const outcome = parsed.data.predicate.outcome;
5049
5227
  if (report.verdict === "insufficient_source") {
5050
5228
  if (outcome.status !== "abstained" || outcome.reason !== report.insufficientSourceReason) {
@@ -5056,15 +5234,17 @@ function verifyWitanAttestationBinding(statement, report, options = {}) {
5056
5234
  return { valid: errors.length === 0, errors };
5057
5235
  }
5058
5236
 
5237
+ // src/witan/index.ts
5238
+ init_esm_shims();
5239
+
5240
+ // src/witan/markdown.ts
5241
+ init_esm_shims();
5242
+
5059
5243
  // src/witan/repo-signals.ts
5060
- import { execFileSync } from "child_process";
5244
+ init_esm_shims();
5061
5245
  import { createHash as createHash2 } from "crypto";
5062
5246
  import {
5063
- closeSync,
5064
5247
  lstatSync,
5065
- openSync,
5066
- readFileSync,
5067
- readSync,
5068
5248
  readdirSync,
5069
5249
  realpathSync,
5070
5250
  statSync
@@ -5072,11 +5252,13 @@ import {
5072
5252
  import { basename, dirname, extname, join, relative, resolve, sep } from "path";
5073
5253
 
5074
5254
  // src/witan/json-safe.ts
5255
+ init_esm_shims();
5075
5256
  function stripBom(text) {
5076
5257
  return text.charCodeAt(0) === 65279 ? text.slice(1) : text;
5077
5258
  }
5078
5259
 
5079
5260
  // src/witan/rubric-version.ts
5261
+ init_esm_shims();
5080
5262
  var WITAN_RUBRIC_VERSION_V9 = "witan-rubric-v9-2026-07-22";
5081
5263
  var WITAN_RUBRIC_VERSION_V10 = "witan-rubric-v10-2026-07-23";
5082
5264
  var WITAN_RUBRIC_VERSION_V11 = "witan-rubric-v11-2026-07-23";
@@ -5093,12 +5275,41 @@ function usesV17DetectorClosure(rubricVersion) {
5093
5275
  return rubricVersion === WITAN_RUBRIC_VERSION_V17 || rubricVersion === WITAN_RUBRIC_VERSION_V18;
5094
5276
  }
5095
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) {
5096
5306
  assertRepoPathExists(options.repoPath);
5097
5307
  const generatedAt = options.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
5098
5308
  const rubricVersion = options.rubricVersion ?? WITAN_RUBRIC_VERSION;
5309
+ const fileInventory = collectRepoFileInventory(options.repoPath);
5099
5310
  const headSha = readGitHead(options.repoPath);
5100
- const repoFiles = listRepoFiles(options.repoPath);
5101
- const inventoryFiles = listRepoInventory(options.repoPath, repoFiles);
5311
+ const repoFiles = fileInventory.repoFiles;
5312
+ const inventoryFiles = fileInventory.inventoryFiles;
5102
5313
  const ignoredTargetReason = repoFiles.length === 0 ? explainIgnoredScanTarget(options.repoPath) : void 0;
5103
5314
  const usesV17Detectors = usesV17DetectorClosure(rubricVersion);
5104
5315
  const usesV18NativeRls = rubricVersion === WITAN_RUBRIC_VERSION_V18;
@@ -5122,6 +5333,7 @@ function buildWitanInputFromRepo(options) {
5122
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);
5123
5334
  const usesV47Detectors = rubricVersion === WITAN_RUBRIC_VERSION_V15 || rubricVersion === WITAN_RUBRIC_VERSION_V16 || usesV17DetectorClosure(rubricVersion);
5124
5335
  const reviewableSourceProof = usesV17Detectors ? buildReviewableSourceProof(options.repoPath, inventoryFiles, headSha, rubricVersion) : void 0;
5336
+ const scanLimitations = new Set(fileInventory.scanLimitations);
5125
5337
  const coreSignals = collectRepoSignals(
5126
5338
  options.repoPath,
5127
5339
  generatedAt,
@@ -5132,7 +5344,8 @@ function buildWitanInputFromRepo(options) {
5132
5344
  usesV39Detectors,
5133
5345
  usesV47Detectors,
5134
5346
  usesV18NativeRls,
5135
- reviewableSourceProof
5347
+ reviewableSourceProof,
5348
+ scanLimitations
5136
5349
  );
5137
5350
  const archetype = usesV17Detectors && reviewableSourceProof?.passes === true && isV17RescuableAbstention(gatedArchetype.abstentionKind) && coreSignals.some(isMeasuredCriterionSignal) ? {
5138
5351
  archetype: isLikelyMonorepo(inventoryFiles) ? "monorepo" : "source",
@@ -5150,6 +5363,7 @@ function buildWitanInputFromRepo(options) {
5150
5363
  ...options.rubricVersion ? { rubricVersion: options.rubricVersion } : {},
5151
5364
  archetype: archetype.archetype,
5152
5365
  ...ignoredTargetReason ?? archetype.insufficientSourceReason ? { insufficientSourceReason: ignoredTargetReason ?? archetype.insufficientSourceReason } : {},
5366
+ ...scanLimitations.size > 0 ? { scanLimitations: [...scanLimitations] } : {},
5153
5367
  signals: [
5154
5368
  ...coreSignals,
5155
5369
  ...(options.domainCollectors ?? []).map((collect) => collect(options.repoPath, repoFiles)),
@@ -5622,26 +5836,21 @@ function isRubyPackageRecipeMetadataSource(repoPath, file) {
5622
5836
  function readSemanticMetadataPrefix(repoPath, file) {
5623
5837
  const absolutePath = join(repoPath, file);
5624
5838
  if (!isRegularFile(absolutePath)) return null;
5625
- let descriptor = null;
5626
- try {
5627
- descriptor = openSync(absolutePath, "r");
5628
- const buffer = Buffer.allocUnsafe(SEMANTIC_METADATA_PREFIX_BYTES_V14);
5629
- const bytesRead = readSync(descriptor, buffer, 0, SEMANTIC_METADATA_PREFIX_BYTES_V14, 0);
5630
- return buffer.subarray(0, bytesRead).toString("utf8");
5631
- } catch {
5632
- return null;
5633
- } finally {
5634
- if (descriptor !== null) closeSync(descriptor);
5635
- }
5839
+ return readRepoTextPrefix(absolutePath, SEMANTIC_METADATA_PREFIX_BYTES_V14);
5636
5840
  }
5637
5841
  function readRepresentativeSourceText(repoPath, file) {
5638
5842
  const absolutePath = join(repoPath, file);
5639
5843
  try {
5640
5844
  if (!isRegularFile(absolutePath)) return null;
5641
5845
  const size = statSync(absolutePath).size;
5642
- if (size === 0 || size > READABLE_SOURCE_REPRESENTATION_FILE_BYTES_V13) return null;
5643
- return readFileSync(absolutePath, "utf8");
5644
- } 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);
5645
5854
  return null;
5646
5855
  }
5647
5856
  }
@@ -5711,28 +5920,25 @@ function assertRepoPathExists(repoPath) {
5711
5920
  }
5712
5921
  }
5713
5922
  function explainIgnoredScanTarget(repoPath) {
5714
- let top;
5715
- try {
5716
- top = execFileSync("git", ["rev-parse", "--show-toplevel"], {
5717
- cwd: repoPath,
5718
- encoding: "utf8",
5719
- stdio: ["ignore", "pipe", "ignore"]
5720
- }).trim();
5721
- } catch {
5722
- 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"));
5723
5927
  }
5928
+ const top = topResult.stdout.trim();
5724
5929
  if (!top) return void 0;
5725
5930
  const topResolved = realpathSync(top);
5726
5931
  const targetResolved = realpathSync(repoPath);
5727
5932
  const target = relative(topResolved, targetResolved).split(sep).join("/");
5728
5933
  if (!target || target.startsWith("../")) return void 0;
5729
- try {
5730
- execFileSync("git", ["check-ignore", "--quiet", "--", `${target}/`], {
5731
- cwd: topResolved,
5732
- stdio: "ignore"
5733
- });
5734
- } catch {
5735
- 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"));
5736
5942
  }
5737
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.`;
5738
5944
  }
@@ -5746,48 +5952,74 @@ function buildNotApplicableSignal(criterionId, reason) {
5746
5952
  notApplicable: true
5747
5953
  };
5748
5954
  }
5749
- 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()) {
5750
5956
  const signals = [];
5751
- const mono = resolveMonorepoContext(repoPath);
5752
- const a1Signal = collectA1TestIntegrityEvidence(
5753
- repoPath,
5754
- repoFiles,
5755
- useV27Detectors,
5756
- useV33Detectors,
5757
- useV36Detectors,
5758
- 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
+ )
5759
5969
  );
5760
- const a2Signal = collectA2IsolationEvidence(
5761
- repoPath,
5762
- repoFiles,
5763
- useV27Detectors,
5764
- useV33Detectors,
5765
- useV36Detectors,
5766
- useV39Detectors,
5767
- useV47Detectors,
5768
- 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
+ )
5769
5983
  );
5770
- const a3Signal = collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors);
5771
- const a4Signal = collectA4DependencyEvidence(
5772
- repoPath,
5773
- repoFiles,
5774
- mono,
5775
- useV27Detectors,
5776
- 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)
5777
6001
  );
5778
- const a5Signal = collectA5ClaimRealityEvidence(repoPath, repoFiles, useV27Detectors);
5779
6002
  const b1Signal = buildNotApplicableSignal(
5780
6003
  "B1",
5781
6004
  "Repository scans do not evaluate the dispatch-trace process dimension."
5782
6005
  );
5783
- const b2Signal = collectB2PrTraceEvidence(repoPath, repoFiles);
5784
- const b3Signal = collectB3CiDisciplineEvidence(repoPath, repoFiles, mono);
5785
- 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
+ );
5786
6015
  const b5Signal = buildNotApplicableSignal(
5787
6016
  "B5",
5788
6017
  "Repository scans do not evaluate the learning-trace process dimension."
5789
6018
  );
5790
- const b6Signal = collectB6PrivilegedOpsGatingEvidence(repoPath, repoFiles, useV39Detectors);
6019
+ const b6Signal = collectCriterion(
6020
+ "B6",
6021
+ () => collectB6PrivilegedOpsGatingEvidence(repoPath, repoFiles, useV39Detectors)
6022
+ );
5791
6023
  for (const signal of [
5792
6024
  a1Signal,
5793
6025
  a2Signal,
@@ -6057,7 +6289,7 @@ function buildA1AuthenticatedAbsenceSignal(repoPath, proof) {
6057
6289
  notes: `V17 authenticated absence: ${proof.eligibleSourceFileCount} criterion-ratable of ${proof.sourceShapedFileCount} source-shaped files; sampled ${proof.sampledFileCount}; ${proof.readableSampledFileCount} representative-text files.`
6058
6290
  };
6059
6291
  }
6060
- function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV18NativeRls) {
6292
+ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV18NativeRls, scanLimitations) {
6061
6293
  const evidence = [];
6062
6294
  const findings = [];
6063
6295
  const gitignore = repoFiles.find((file) => basename(file) === ".gitignore");
@@ -6103,6 +6335,7 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
6103
6335
  repoPath,
6104
6336
  useV27Detectors,
6105
6337
  currentSecretFingerprintsByPath,
6338
+ scanLimitations,
6106
6339
  useV33Detectors,
6107
6340
  useV36Detectors,
6108
6341
  useV39Detectors,
@@ -6119,7 +6352,7 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
6119
6352
  const committedEnvFilePath = repoFiles.find(
6120
6353
  (f) => /(?:^|\/)\.env(?:\.|$)/i.test(f) && !isEnvTemplatePath(f, useV47Detectors) && (!useV47Detectors || isV47AuthoredProductionPath(f)) && (!useV39Detectors || !hasTemplateOnlyEnvContent(repoPath, f))
6121
6354
  );
6122
- const hasEnvInHistory = committedEnvFilePath === void 0 && hasEnvPathInGitHistory(repoPath);
6355
+ const hasEnvInHistory = committedEnvFilePath === void 0 && hasEnvPathInGitHistory(repoPath, scanLimitations);
6123
6356
  const dbLayerImportFile = migrationFiles.length === 0 ? implFiles.slice(0, 30).find((f) => fileContains(repoPath, f, DB_CLIENT_PATTERN)) : void 0;
6124
6357
  const hasDataLayer = migrationFiles.length > 0 || dbLayerImportFile !== void 0;
6125
6358
  const hasSecretsSurface = envExamples.length > 0 || committedEnvFilePath !== void 0 || gitignoreHasEnvRule || hasEnvInHistory;
@@ -7279,14 +7512,14 @@ function lineHasInsecureSecretCompare(line, useV39Detectors = false) {
7279
7512
  function findInsecureSecretCompareLine(repoPath, file, useV39Detectors = false) {
7280
7513
  const fullPath = join(repoPath, file);
7281
7514
  if (!isRegularFile(fullPath)) return null;
7282
- const lines = readFileSync(fullPath, "utf8").split("\n");
7515
+ const lines = readRepoText(fullPath, "utf8").split("\n");
7283
7516
  const index = lines.findIndex((line) => lineHasInsecureSecretCompare(line, useV39Detectors));
7284
7517
  return index === -1 ? null : index + 1;
7285
7518
  }
7286
7519
  function findFirstMatchingLine(repoPath, file, pattern) {
7287
7520
  const fullPath = join(repoPath, file);
7288
7521
  if (!isRegularFile(fullPath)) return null;
7289
- const lines = readFileSync(fullPath, "utf8").split("\n");
7522
+ const lines = readRepoText(fullPath, "utf8").split("\n");
7290
7523
  const index = lines.findIndex((line) => pattern.test(line));
7291
7524
  return index === -1 ? null : index + 1;
7292
7525
  }
@@ -7294,7 +7527,7 @@ function lineAt(repoPath, file, line) {
7294
7527
  if (line < 1) return null;
7295
7528
  const fullPath = join(repoPath, file);
7296
7529
  if (!isRegularFile(fullPath)) return null;
7297
- return readFileSync(fullPath, "utf8").split("\n")[line - 1] ?? null;
7530
+ return readRepoText(fullPath, "utf8").split("\n")[line - 1] ?? null;
7298
7531
  }
7299
7532
  var UNSORTED_JSON_SIGN_PATTERN = /\.update\s*\(\s*JSON\.stringify\s*\(|\.sign\s*\([^)]*JSON\.stringify\s*\(/;
7300
7533
  var UNSORTED_JSON_DIRECT_SIGN_PATTERN = /\.sign\s*\([^)]*JSON\.stringify\s*\(/;
@@ -7303,7 +7536,7 @@ var JSON_STRINGIFY_ASSIGNMENT_PATTERN = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*
7303
7536
  function findUnsortedJsonHmacLine(repoPath, file) {
7304
7537
  const fullPath = join(repoPath, file);
7305
7538
  if (!isRegularFile(fullPath)) return null;
7306
- const contents = readFileSync(fullPath, "utf8");
7539
+ const contents = readRepoText(fullPath, "utf8");
7307
7540
  const directSign = findFirstMatchingLine(repoPath, file, UNSORTED_JSON_DIRECT_SIGN_PATTERN);
7308
7541
  if (directSign !== null) return directSign;
7309
7542
  const directConstructorUpdate = findFirstMatchingLine(
@@ -7384,27 +7617,92 @@ function isEnvTemplatePath(path, useV47Detectors = false) {
7384
7617
  }
7385
7618
  var HARD_EXCLUDED_PATH_PATTERN = /(^|\/)(?:\.git|\.venv|venv|site-packages|node_modules|dist|build|\.next|__pycache__|vendor|\.terraform|coverage)(?:\/|$)/;
7386
7619
  var COVERAGE_PERCENT_PATTERN = /(?:statements|branches|lines|functions|fail_under)\s*[:=]\s*["']?(\d+(?:\.\d+)?)/gi;
7387
- function listRepoFiles(repoPath) {
7388
- const gitFiles = listGitTrackedFiles(repoPath, false);
7389
- 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) {
7390
7632
  const files = [];
7391
- 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);
7392
7660
  return files.filter((file) => !isHardExcludedPath(file)).sort();
7393
7661
  }
7394
- function listRepoInventory(repoPath, fallbackFiles) {
7395
- 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
+ };
7396
7686
  }
7397
- function resolveMonorepoContext(repoPath) {
7398
- let top;
7399
- try {
7400
- top = execFileSync("git", ["rev-parse", "--show-toplevel"], {
7401
- cwd: repoPath,
7402
- encoding: "utf8",
7403
- stdio: ["ignore", "pipe", "ignore"]
7404
- }).trim();
7405
- } 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
+ );
7406
7703
  return null;
7407
7704
  }
7705
+ const top = topResult.stdout.trim();
7408
7706
  if (!top) return null;
7409
7707
  const rootResolved = resolve(top);
7410
7708
  const repoResolved = (() => {
@@ -7416,7 +7714,7 @@ function resolveMonorepoContext(repoPath) {
7416
7714
  })();
7417
7715
  if (rootResolved === repoResolved) return null;
7418
7716
  if (!repoResolved.startsWith(rootResolved + sep)) return null;
7419
- const rootFiles = listGitTrackedFiles(rootResolved);
7717
+ const rootFiles = listGitTrackedFiles(rootResolved, scanLimitations);
7420
7718
  if (!rootFiles || rootFiles.length === 0) return null;
7421
7719
  const subRel = repoResolved.slice(rootResolved.length + 1).split(sep).join("/");
7422
7720
  const subPrefix = subRel ? `${subRel}/` : "";
@@ -7426,50 +7724,119 @@ function resolveMonorepoContext(repoPath) {
7426
7724
  if (sharedFiles.length === 0) return null;
7427
7725
  return { root: rootResolved, sharedFiles };
7428
7726
  }
7429
- function listGitTrackedFiles(repoPath, includeHardExcluded = false) {
7430
- try {
7431
- execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
7432
- cwd: repoPath,
7433
- encoding: "utf8",
7434
- stdio: ["ignore", "pipe", "ignore"]
7435
- });
7436
- const output = execFileSync("git", ["ls-files", "--cached"], {
7437
- cwd: repoPath,
7438
- encoding: "utf8",
7439
- stdio: ["ignore", "pipe", "ignore"]
7440
- }).trim();
7441
- if (!output) return [];
7442
- return output.split("\n").filter(
7443
- (file) => file.length > 0 && (includeHardExcluded || !isHardExcludedPath(file)) && isRegularFile(join(repoPath, file))
7444
- ).sort();
7445
- } catch {
7446
- return null;
7447
- }
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;
7448
7735
  }
7449
- function visitRepoDir(repoPath, dirPath, files) {
7736
+ function visitRepoDir(repoPath, dirPath, files, rootDevice) {
7450
7737
  let entries;
7451
7738
  try {
7452
7739
  entries = readdirSync(dirPath, { withFileTypes: true });
7453
- } catch {
7740
+ } catch (error) {
7741
+ recordFilesystemSkip(dirPath, error, true);
7454
7742
  return;
7455
7743
  }
7456
7744
  for (const entry of entries) {
7457
- if (shouldSkipDir(entry.name)) continue;
7458
7745
  const fullPath = join(dirPath, entry.name);
7746
+ if (shouldSkipDir(entry.name)) {
7747
+ recordContentSkip(fullPath, "denied_path");
7748
+ continue;
7749
+ }
7459
7750
  if (entry.isDirectory()) {
7460
- 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");
7461
7765
  continue;
7462
7766
  }
7463
- if (!entry.isFile()) continue;
7464
7767
  let size;
7465
7768
  try {
7466
7769
  size = statSync(fullPath).size;
7467
- } catch {
7770
+ } catch (error) {
7771
+ recordFilesystemSkip(fullPath, error);
7468
7772
  continue;
7469
7773
  }
7470
- if (size > 512e3) continue;
7471
- files.push(relative(repoPath, fullPath));
7472
- }
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);
7473
7840
  }
7474
7841
  function shouldSkipDir(name) {
7475
7842
  return [
@@ -7496,7 +7863,7 @@ function collectContentBasedCppTestFiles(repoPath, repoFiles) {
7496
7863
  return candidates.filter((file) => {
7497
7864
  const fullPath = join(repoPath, file);
7498
7865
  if (!isRegularFile(fullPath)) return false;
7499
- const contents = readFileSync(fullPath, "utf8");
7866
+ const contents = readRepoText(fullPath, "utf8");
7500
7867
  return CPP_TEST_FRAMEWORK_PATTERN.test(contents) || CPP_TEST_MACRO_PATTERN.test(contents);
7501
7868
  });
7502
7869
  }
@@ -7516,7 +7883,7 @@ function measureNonHollowTestShare(repoPath, files, useV17Detectors = false) {
7516
7883
  for (const file of files) {
7517
7884
  const fullPath = join(repoPath, file);
7518
7885
  if (!isRegularFile(fullPath)) continue;
7519
- const contents = readFileSync(fullPath, "utf8");
7886
+ const contents = readRepoText(fullPath, "utf8");
7520
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);
7521
7888
  if (nonHollow) {
7522
7889
  nonHollowCount += 1;
@@ -7537,7 +7904,7 @@ function countPatternMatches(repoPath, files, pattern) {
7537
7904
  for (const file of files) {
7538
7905
  const fullPath = join(repoPath, file);
7539
7906
  if (!isRegularFile(fullPath)) continue;
7540
- count += [...readFileSync(fullPath, "utf8").matchAll(matcher)].length;
7907
+ count += [...readRepoText(fullPath, "utf8").matchAll(matcher)].length;
7541
7908
  }
7542
7909
  return count;
7543
7910
  }
@@ -7545,7 +7912,7 @@ function readCoveragePercent(repoPath, files) {
7545
7912
  for (const file of files) {
7546
7913
  const fullPath = join(repoPath, file);
7547
7914
  if (!isRegularFile(fullPath)) continue;
7548
- const contents = readFileSync(fullPath, "utf8");
7915
+ const contents = readRepoText(fullPath, "utf8");
7549
7916
  const summaryPercent = parseCoverageSummaryPercent(contents);
7550
7917
  if (summaryPercent !== null) return summaryPercent;
7551
7918
  const thresholdPercent = parseCoverageThresholdPercent(contents);
@@ -7635,7 +8002,7 @@ function readDependencySpecs(repoPath, manifests) {
7635
8002
  }
7636
8003
  function readGoModDependencySpecs(path) {
7637
8004
  const specs = [];
7638
- const contents = readFileSync(path, "utf8");
8005
+ const contents = readRepoText(path, "utf8");
7639
8006
  const blockPattern = /require\s*\(([^)]+)\)/gs;
7640
8007
  const singlePattern = /^require\s+(\S+)\s+(\S+)/gm;
7641
8008
  for (const blockMatch of contents.matchAll(blockPattern)) {
@@ -7656,7 +8023,7 @@ function readGoModDependencySpecs(path) {
7656
8023
  }
7657
8024
  function readCargoTomlDependencySpecs(path) {
7658
8025
  const specs = [];
7659
- const contents = readFileSync(path, "utf8");
8026
+ const contents = readRepoText(path, "utf8");
7660
8027
  const depSectionPattern = /^\[(?:dependencies|dev-dependencies|build-dependencies)\]([\s\S]*?)(?=^\[|(?![\s\S]))/gm;
7661
8028
  for (const sectionMatch of contents.matchAll(depSectionPattern)) {
7662
8029
  const section = sectionMatch[1] ?? "";
@@ -7678,7 +8045,7 @@ function readCargoTomlDependencySpecs(path) {
7678
8045
  }
7679
8046
  function readPyprojectDependencySpecs(path) {
7680
8047
  const specs = [];
7681
- const contents = readFileSync(path, "utf8");
8048
+ const contents = readRepoText(path, "utf8");
7682
8049
  const poetrySectionPattern = /^\[tool\.poetry\.dependencies\]([\s\S]*?)(?=^\[|(?![\s\S]))/gm;
7683
8050
  for (const sectionMatch of contents.matchAll(poetrySectionPattern)) {
7684
8051
  const section = sectionMatch[1] ?? "";
@@ -7712,7 +8079,7 @@ function readPyprojectDependencySpecs(path) {
7712
8079
  }
7713
8080
  function readPipfileDependencySpecs(path) {
7714
8081
  const specs = [];
7715
- const contents = readFileSync(path, "utf8");
8082
+ const contents = readRepoText(path, "utf8");
7716
8083
  const sectionPattern = /^\[(?:packages|dev-packages)\]([\s\S]*?)(?=^\[|$(?![\s\S]))/gm;
7717
8084
  for (const sectionMatch of contents.matchAll(sectionPattern)) {
7718
8085
  for (const line of (sectionMatch[1] ?? "").split("\n")) {
@@ -7725,7 +8092,7 @@ function readPipfileDependencySpecs(path) {
7725
8092
  return specs;
7726
8093
  }
7727
8094
  function readVcpkgJsonDependencySpecs(path) {
7728
- const parsed = parseJsonObject(readFileSync(path, "utf8"));
8095
+ const parsed = parseJsonObject(readRepoText(path, "utf8"));
7729
8096
  if (!parsed) return [];
7730
8097
  const rawDeps = Array.isArray(parsed.dependencies) ? parsed.dependencies : [];
7731
8098
  const specs = [];
@@ -7741,7 +8108,7 @@ function readVcpkgJsonDependencySpecs(path) {
7741
8108
  }
7742
8109
  function readConanfileTxtDependencySpecs(path) {
7743
8110
  const specs = [];
7744
- const contents = readFileSync(path, "utf8");
8111
+ const contents = readRepoText(path, "utf8");
7745
8112
  const requiresMatch = /^\[requires\]([\s\S]*?)(?=^\[|$(?![\s\S]))/m.exec(contents);
7746
8113
  if (!requiresMatch) return specs;
7747
8114
  for (const line of (requiresMatch[1] ?? "").split("\n")) {
@@ -7754,7 +8121,7 @@ function readConanfileTxtDependencySpecs(path) {
7754
8121
  }
7755
8122
  function readConanfilePyDependencySpecs(path) {
7756
8123
  const specs = [];
7757
- const contents = readFileSync(path, "utf8");
8124
+ const contents = readRepoText(path, "utf8");
7758
8125
  for (const match of contents.matchAll(/self\.requires\s*\(\s*["']([\w.\-]+)\/([\w.\-]+)["']/g)) {
7759
8126
  specs.push({ name: match[1] ?? "", version: match[2] ?? "" });
7760
8127
  }
@@ -7768,7 +8135,7 @@ function readConanfilePyDependencySpecs(path) {
7768
8135
  }
7769
8136
  function readCMakeDependencySpecs(path) {
7770
8137
  const specs = [];
7771
- const contents = readFileSync(path, "utf8");
8138
+ const contents = readRepoText(path, "utf8");
7772
8139
  for (const match of contents.matchAll(/find_package\s*\(\s*([\w]+)(?:\s+([\d.]+))?/gi)) {
7773
8140
  const name = match[1];
7774
8141
  if (!name) continue;
@@ -7783,7 +8150,7 @@ function readCMakeDependencySpecs(path) {
7783
8150
  return specs;
7784
8151
  }
7785
8152
  function readPackageJsonDependencySpecs(packageJsonPath) {
7786
- const parsed = parseJsonObject(readFileSync(packageJsonPath, "utf8"));
8153
+ const parsed = parseJsonObject(readRepoText(packageJsonPath, "utf8"));
7787
8154
  if (!parsed) return [];
7788
8155
  const optionalPeerNames = /* @__PURE__ */ new Set();
7789
8156
  const peerDependenciesMeta = parsed.peerDependenciesMeta;
@@ -7810,7 +8177,7 @@ function readPackageJsonDependencySpecs(packageJsonPath) {
7810
8177
  return specs;
7811
8178
  }
7812
8179
  function readRequirementsDependencySpecs(path) {
7813
- 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) => {
7814
8181
  const [name = line, version = ""] = line.split(/[<>=~!]=?/, 2);
7815
8182
  return { name: name.trim(), version: line.includes("==") ? version.trim() : line };
7816
8183
  });
@@ -7828,7 +8195,7 @@ function isPinnedDependencyVersion(version) {
7828
8195
  function workflowTargetsDefaultBranch(repoPath, file) {
7829
8196
  const fullPath = join(repoPath, file);
7830
8197
  if (!isRegularFile(fullPath)) return false;
7831
- const contents = readFileSync(fullPath, "utf8").toLowerCase();
8198
+ const contents = readRepoText(fullPath, "utf8").toLowerCase();
7832
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);
7833
8200
  }
7834
8201
  function isPrMergeCommit(commit) {
@@ -7982,7 +8349,7 @@ function deriveRlsPolicyScopeInventory(repoPath, repoFiles) {
7982
8349
  for (const file of candidatePolicyFiles) {
7983
8350
  let content;
7984
8351
  try {
7985
- content = readFileSync(join(repoPath, file), "utf8");
8352
+ content = readRepoText(join(repoPath, file), "utf8");
7986
8353
  } catch {
7987
8354
  continue;
7988
8355
  }
@@ -8086,7 +8453,7 @@ function findClaimRealityReconciliationArtifacts(repoPath, repoFiles) {
8086
8453
  function isAuthenticatedClaimRealityArtifact(repoPath, file) {
8087
8454
  const fullPath = join(repoPath, file);
8088
8455
  if (!isRegularFile(fullPath)) return false;
8089
- const contents = readFileSync(fullPath, "utf8");
8456
+ const contents = readRepoText(fullPath, "utf8");
8090
8457
  if (/\.json$/i.test(file)) {
8091
8458
  return parseJsonObject(contents)?.artifactKind === "claim_reality_reconciliation";
8092
8459
  }
@@ -8125,7 +8492,7 @@ function isCoverageConfig(repoPath, file) {
8125
8492
  function packageJsonHasCoverageTooling(repoPath, file) {
8126
8493
  const fullPath = join(repoPath, file);
8127
8494
  if (!isRegularFile(fullPath)) return false;
8128
- const parsed = parseJsonObject(readFileSync(fullPath, "utf8"));
8495
+ const parsed = parseJsonObject(readRepoText(fullPath, "utf8"));
8129
8496
  if (!parsed) return false;
8130
8497
  const scripts = parsed.scripts;
8131
8498
  if (typeof scripts === "object" && scripts !== null && !Array.isArray(scripts)) {
@@ -8599,7 +8966,7 @@ function findRuntimeContainerEntrypointFile(repoPath, repoFiles) {
8599
8966
  return repoFiles.filter((file) => /(^|\/)Dockerfile$/i.test(file) && isAuthoredProductionPath(file)).find((file) => {
8600
8967
  const fullPath = join(repoPath, file);
8601
8968
  if (!isRegularFile(fullPath)) return false;
8602
- const lines = readFileSync(fullPath, "utf8").split(/\r?\n/);
8969
+ const lines = readRepoText(fullPath, "utf8").split(/\r?\n/);
8603
8970
  const finalStageStart = lines.reduce(
8604
8971
  (last, line, index) => /^\s*FROM\b/i.test(line) ? index : last,
8605
8972
  0
@@ -8648,7 +9015,7 @@ function packageJsonHasAuditScript(repoPath, manifest) {
8648
9015
  function packageJsonHasHeavyTestDependency(repoPath, manifest) {
8649
9016
  const fullPath = join(repoPath, manifest);
8650
9017
  if (!isRegularFile(fullPath)) return false;
8651
- const parsed = parseJsonObject(readFileSync(fullPath, "utf8"));
9018
+ const parsed = parseJsonObject(readRepoText(fullPath, "utf8"));
8652
9019
  if (!parsed) return false;
8653
9020
  const names = [
8654
9021
  ...Object.keys(isRecord(parsed.dependencies) ? parsed.dependencies : {}),
@@ -8659,7 +9026,7 @@ function packageJsonHasHeavyTestDependency(repoPath, manifest) {
8659
9026
  function packageJsonDescribesPackagedApplication(repoPath, manifest, repoFiles) {
8660
9027
  const fullPath = join(repoPath, manifest);
8661
9028
  if (!isRegularFile(fullPath)) return false;
8662
- const parsed = parseJsonObject(readFileSync(fullPath, "utf8"));
9029
+ const parsed = parseJsonObject(readRepoText(fullPath, "utf8"));
8663
9030
  if (!parsed) return false;
8664
9031
  const scripts = isRecord(parsed.scripts) ? Object.values(parsed.scripts) : [];
8665
9032
  const dependencies = {
@@ -8684,7 +9051,7 @@ function findTauriApplicationMetadata(repoPath, manifest, repoFiles) {
8684
9051
  if (!repoFiles.includes(file)) return false;
8685
9052
  const fullPath = join(repoPath, file);
8686
9053
  if (!isRegularFile(fullPath)) return false;
8687
- const config = parseJsonObject(readFileSync(fullPath, "utf8"));
9054
+ const config = parseJsonObject(readRepoText(fullPath, "utf8"));
8688
9055
  const packageMetadata = isRecord(config?.package) ? config.package : null;
8689
9056
  return isNonEmptyString(config?.productName) || isNonEmptyString(config?.identifier) || packageMetadata != null && isNonEmptyString(packageMetadata.productName);
8690
9057
  });
@@ -8731,12 +9098,12 @@ function isRegularFile(path) {
8731
9098
  function fileContains(repoPath, file, pattern) {
8732
9099
  const fullPath = join(repoPath, file);
8733
9100
  if (!isRegularFile(fullPath)) return false;
8734
- return pattern.test(readFileSync(fullPath, "utf8"));
9101
+ return pattern.test(readRepoText(fullPath, "utf8"));
8735
9102
  }
8736
9103
  function findCommittedSecretInFile(repoPath, file, useV36Detectors = false, useV39Detectors = false, useV47Detectors = false) {
8737
9104
  const fullPath = join(repoPath, file);
8738
9105
  if (!isRegularFile(fullPath)) return null;
8739
- const contents = readFileSync(fullPath, "utf8");
9106
+ const contents = readRepoText(fullPath, "utf8");
8740
9107
  const isTestCredentialConfiguration = useV39Detectors && isLikelyTestCredentialConfiguration(file, contents);
8741
9108
  const secretScanContents = prepareCredentialScanContents(
8742
9109
  contents,
@@ -8829,7 +9196,7 @@ function isLikelyTestCredentialConfiguration(file, contents) {
8829
9196
  function hasTemplateOnlyEnvContent(repoPath, file) {
8830
9197
  const fullPath = join(repoPath, file);
8831
9198
  if (!isRegularFile(fullPath)) return false;
8832
- 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));
8833
9200
  if (substantiveLines.length === 0) return true;
8834
9201
  const assignments = substantiveLines.map(
8835
9202
  (line) => /^\s*(?:export\s+)?[A-Za-z_][A-Za-z0-9_]*\s*=\s*(.*?)\s*$/.exec(line)?.[1] ?? null
@@ -8844,7 +9211,7 @@ function hasTemplateOnlyEnvContent(repoPath, file) {
8844
9211
  function findSecretFingerprintsInFile(repoPath, file, useV36Detectors = false, useV39Detectors = false, useV47Detectors = false) {
8845
9212
  const fullPath = join(repoPath, file);
8846
9213
  if (!isRegularFile(fullPath)) return [];
8847
- const contents = readFileSync(fullPath, "utf8");
9214
+ const contents = readRepoText(fullPath, "utf8");
8848
9215
  const scanContents = prepareCredentialScanContents(
8849
9216
  contents,
8850
9217
  file,
@@ -8995,8 +9362,18 @@ function secretEvidenceLabel(prefix, match) {
8995
9362
  function isSecretIdentifierCharacter(characterCode) {
8996
9363
  return characterCode >= 48 && characterCode <= 57 || characterCode >= 65 && characterCode <= 90 || characterCode === 95 || characterCode >= 97 && characterCode <= 122;
8997
9364
  }
8998
- function collectHistorySecretEvidence(repoPath, excludeHead, currentSecretFingerprintsByPath, authoredProductionOnly = false, useV36Detectors = false, useV39Detectors = false, useV47Detectors = false) {
8999
- 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
+ );
9000
9377
  const head = excludeHead ? readGitHead(repoPath) : null;
9001
9378
  let scannedFiles = 0;
9002
9379
  let envPathEvidence = null;
@@ -9009,7 +9386,7 @@ function collectHistorySecretEvidence(repoPath, excludeHead, currentSecretFinger
9009
9386
  return { evidence: null, envPathEvidence, truncated: true };
9010
9387
  }
9011
9388
  scannedFiles += 1;
9012
- const contents = readGitBlob(repoPath, entry.commit, entry.path);
9389
+ const contents = readGitBlob(repoPath, entry.commit, entry.path, scanLimitations);
9013
9390
  if (!contents) continue;
9014
9391
  const scanContents = prepareCredentialScanContents(
9015
9392
  contents,
@@ -9052,7 +9429,7 @@ function collectHistorySecretEvidence(repoPath, excludeHead, currentSecretFinger
9052
9429
  }
9053
9430
  return { evidence: null, envPathEvidence, truncated: false };
9054
9431
  }
9055
- function readCredentialHistoryEntries(repoPath, useV36Detectors = false) {
9432
+ function readCredentialHistoryEntries(repoPath, scanLimitations, useV36Detectors = false) {
9056
9433
  const entries = [];
9057
9434
  const seen = /* @__PURE__ */ new Set();
9058
9435
  const appendEntry = (entry) => {
@@ -9061,14 +9438,12 @@ function readCredentialHistoryEntries(repoPath, useV36Detectors = false) {
9061
9438
  seen.add(key);
9062
9439
  entries.push(entry);
9063
9440
  };
9064
- try {
9065
- const commits = execFileSync("git", ["rev-list", "HEAD"], {
9066
- cwd: repoPath,
9067
- encoding: "utf8",
9068
- stdio: ["ignore", "pipe", "ignore"]
9069
- });
9070
- const output = execFileSync(
9071
- "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(
9072
9447
  [
9073
9448
  "diff-tree",
9074
9449
  "--stdin",
@@ -9076,39 +9451,53 @@ function readCredentialHistoryEntries(repoPath, useV36Detectors = false) {
9076
9451
  "--name-only",
9077
9452
  "-r",
9078
9453
  "--diff-filter=AM",
9079
- "--pretty=format:commit:%H"
9454
+ "-z",
9455
+ "--pretty=format:commit:%H%x00"
9080
9456
  ],
9081
- {
9082
- cwd: repoPath,
9083
- encoding: "utf8",
9084
- input: commits,
9085
- stdio: ["pipe", "pipe", "ignore"]
9086
- }
9457
+ { cwd: repoPath, input: commitsResult.stdout }
9087
9458
  );
9088
- let currentCommit = null;
9089
- for (const line of output.split("\n")) {
9090
- if (line.startsWith("commit:")) {
9091
- currentCommit = line.slice("commit:".length);
9092
- 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 });
9093
9479
  }
9094
- const path = line.trim();
9095
- if (!currentCommit || !path) continue;
9096
- if (!isCredentialHistoryPath(path, useV36Detectors)) continue;
9097
- appendEntry({ commit: currentCommit, path });
9098
9480
  }
9099
- } catch {
9100
9481
  }
9101
- for (const entry of readDeletedCredentialHistoryEntries(repoPath, useV36Detectors)) {
9482
+ for (const entry of readDeletedCredentialHistoryEntries(
9483
+ repoPath,
9484
+ scanLimitations,
9485
+ useV36Detectors
9486
+ )) {
9102
9487
  appendEntry(entry);
9103
9488
  }
9104
9489
  return entries;
9105
9490
  }
9106
- function readDeletedCredentialHistoryEntries(repoPath, useV36Detectors = false) {
9107
- const deletedPaths = readDeletedCredentialHistoryPaths(repoPath, useV36Detectors);
9491
+ function readDeletedCredentialHistoryEntries(repoPath, scanLimitations, useV36Detectors = false) {
9492
+ const deletedPaths = readDeletedCredentialHistoryPaths(
9493
+ repoPath,
9494
+ scanLimitations,
9495
+ useV36Detectors
9496
+ );
9108
9497
  const entries = [];
9109
9498
  const seen = /* @__PURE__ */ new Set();
9110
9499
  for (const path of deletedPaths) {
9111
- for (const commit of readHistoryCommitsForPath(repoPath, path)) {
9500
+ for (const commit of readHistoryCommitsForPath(repoPath, path, scanLimitations)) {
9112
9501
  const key = `${commit}:${path}`;
9113
9502
  if (seen.has(key)) continue;
9114
9503
  seen.add(key);
@@ -9117,47 +9506,51 @@ function readDeletedCredentialHistoryEntries(repoPath, useV36Detectors = false)
9117
9506
  }
9118
9507
  return entries;
9119
9508
  }
9120
- function readDeletedCredentialHistoryPaths(repoPath, useV36Detectors = false) {
9121
- try {
9122
- const output = execFileSync(
9123
- "git",
9124
- ["log", "HEAD", "--diff-filter=D", "--name-status", "--format=commit:%H"],
9125
- {
9126
- cwd: repoPath,
9127
- encoding: "utf8",
9128
- stdio: ["ignore", "pipe", "ignore"]
9129
- }
9130
- );
9131
- const paths = [];
9132
- const seen = /* @__PURE__ */ new Set();
9133
- for (const line of output.split("\n")) {
9134
- if (!line.startsWith("D ")) continue;
9135
- const path = line.slice(2).trim();
9136
- if (!path || seen.has(path)) continue;
9137
- if (!isCredentialHistoryPath(path, useV36Detectors)) continue;
9138
- seen.add(path);
9139
- paths.push(path);
9140
- }
9141
- return paths;
9142
- } 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");
9143
9525
  return [];
9144
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;
9145
9542
  }
9146
- function readHistoryCommitsForPath(repoPath, path) {
9147
- try {
9148
- const output = execFileSync(
9149
- "git",
9150
- ["log", "HEAD", "--diff-filter=AM", "--format=%H", "--", path],
9151
- {
9152
- cwd: repoPath,
9153
- encoding: "utf8",
9154
- stdio: ["ignore", "pipe", "ignore"]
9155
- }
9156
- );
9157
- return output.split("\n").map((line) => line.trim()).filter((line) => /^[0-9a-f]{40}$/i.test(line));
9158
- } 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");
9159
9551
  return [];
9160
9552
  }
9553
+ return historyResult.stdout.split("\n").map((line) => line.trim()).filter((line) => /^[0-9a-f]{40}$/i.test(line));
9161
9554
  }
9162
9555
  function isCredentialHistoryPath(path, _useV36Detectors = false) {
9163
9556
  if (isHardExcludedPath(path)) return false;
@@ -9174,17 +9567,23 @@ function isEnvHistoryPath(path, useV36Detectors = false, useV47Detectors = false
9174
9567
  const basename3 = path.toLowerCase().split("/").at(-1) ?? path.toLowerCase();
9175
9568
  return (useV36Detectors ? /^\.env(?:\.|$)/ : /\.env(?:\.|$)/).test(basename3);
9176
9569
  }
9177
- function readGitBlob(repoPath, commit, path) {
9178
- try {
9179
- return execFileSync("git", ["show", `${commit}:${path}`], {
9180
- cwd: repoPath,
9181
- encoding: "utf8",
9182
- maxBuffer: 512e3,
9183
- stdio: ["ignore", "pipe", "ignore"]
9184
- });
9185
- } 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
+ );
9186
9584
  return null;
9187
9585
  }
9586
+ return blobResult.stdout;
9188
9587
  }
9189
9588
  var ALL_CAPS_SNAKE_PLACEHOLDER_PATTERN = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/;
9190
9589
  function isPlaceholderSecretValue(value) {
@@ -9214,7 +9613,7 @@ function hasSuspiciousDependencies(repoPath, manifest, useV47Detectors = false)
9214
9613
  if (!manifest.endsWith("package.json")) return false;
9215
9614
  const fullPath = join(repoPath, manifest);
9216
9615
  if (!isRegularFile(fullPath)) return false;
9217
- const parsed = parseJsonObject(readFileSync(fullPath, "utf8"));
9616
+ const parsed = parseJsonObject(readRepoText(fullPath, "utf8"));
9218
9617
  if (!parsed) return false;
9219
9618
  const dependencies = isRecord(parsed.dependencies) ? parsed.dependencies : {};
9220
9619
  const devDependencies = isRecord(parsed.devDependencies) ? parsed.devDependencies : {};
@@ -9230,24 +9629,26 @@ function hasSuspiciousDependencies(repoPath, manifest, useV47Detectors = false)
9230
9629
  return placeholderIdentity && placeholderMetadata;
9231
9630
  }
9232
9631
  function readRecentCommits(repoPath) {
9233
- try {
9234
- const output = execFileSync("git", ["log", "--max-count=12", "--format=%H%x00%s"], {
9235
- cwd: repoPath,
9236
- encoding: "utf8",
9237
- stdio: ["ignore", "pipe", "ignore"]
9238
- }).trim();
9239
- if (!output) return [];
9240
- return output.split("\n").map((line) => {
9241
- const [sha = "", subject = ""] = line.split("\0");
9242
- return { sha, subject };
9243
- }).filter((commit) => commit.sha.length >= 7);
9244
- } catch {
9245
- 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"));
9246
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);
9247
9648
  }
9248
9649
  function readPackageScripts(packageJsonPath) {
9249
9650
  if (!isRegularFile(packageJsonPath)) return /* @__PURE__ */ new Map();
9250
- const parsed = parseJsonObject(readFileSync(packageJsonPath, "utf8"));
9651
+ const parsed = parseJsonObject(readRepoText(packageJsonPath, "utf8"));
9251
9652
  const scripts = parsed?.scripts;
9252
9653
  if (!isRecord(scripts)) return /* @__PURE__ */ new Map();
9253
9654
  return new Map(
@@ -9258,7 +9659,7 @@ function readPackageScripts(packageJsonPath) {
9258
9659
  }
9259
9660
  function evidenceForRelative(repoPath, path, kind, label) {
9260
9661
  const fullPath = join(repoPath, path);
9261
- const contents = isRegularFile(fullPath) ? readFileSync(fullPath, "utf8") : "";
9662
+ const contents = isRegularFile(fullPath) ? readRepoText(fullPath, "utf8") : "";
9262
9663
  return {
9263
9664
  kind,
9264
9665
  label,
@@ -9271,7 +9672,7 @@ function evidenceForRelative(repoPath, path, kind, label) {
9271
9672
  }
9272
9673
  function evidenceForRelativeAtLine(repoPath, path, kind, label, line) {
9273
9674
  const fullPath = join(repoPath, path);
9274
- const contents = isRegularFile(fullPath) ? readFileSync(fullPath, "utf8") : "";
9675
+ const contents = isRegularFile(fullPath) ? readRepoText(fullPath, "utf8") : "";
9275
9676
  return {
9276
9677
  kind,
9277
9678
  label,
@@ -9298,43 +9699,46 @@ function firstMeaningfulLine(contents) {
9298
9699
  return index === -1 ? 1 : index + 1;
9299
9700
  }
9300
9701
  function readGitHead(repoPath) {
9301
- try {
9302
- return execFileSync("git", ["rev-parse", "HEAD"], {
9303
- cwd: repoPath,
9304
- encoding: "utf8",
9305
- stdio: ["ignore", "pipe", "ignore"]
9306
- }).trim();
9307
- } catch {
9308
- 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"));
9309
9706
  }
9707
+ return headResult.stdout.trim() || null;
9310
9708
  }
9311
- function hasEnvPathInGitHistory(repoPath) {
9312
- try {
9313
- const output = execFileSync(
9314
- "git",
9315
- ["log", "HEAD", "--diff-filter=AM", "--name-only", "--format="],
9316
- {
9317
- cwd: repoPath,
9318
- encoding: "utf8",
9319
- stdio: ["ignore", "pipe", "ignore"],
9320
- maxBuffer: 512e3
9321
- }
9322
- ).trim();
9323
- if (!output) return false;
9324
- return output.split("\n").some((p) => /(?:^|\/)\.env(?:\.|$)/i.test(p.trim()));
9325
- } 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");
9326
9717
  return false;
9327
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
+ });
9328
9729
  }
9329
9730
 
9330
9731
  // src/witan/public-scan.ts
9732
+ init_esm_shims();
9331
9733
  import { resolve as resolve2 } from "path";
9332
9734
 
9333
9735
  // src/witan/ingest.ts
9334
- import { readFileSync as readFileSync5, readdirSync as readdirSync2 } from "fs";
9736
+ init_esm_shims();
9737
+ import { readFileSync as readFileSync4, readdirSync as readdirSync2 } from "fs";
9335
9738
  import { join as join2 } from "path";
9336
9739
 
9337
9740
  // src/witan/finding-limits.ts
9741
+ init_esm_shims();
9338
9742
  var RULE_ID_MAX = 200;
9339
9743
  var MESSAGE_MAX = 500;
9340
9744
  var LOCATION_MAX = 700;
@@ -9353,7 +9757,8 @@ function clampFinding(finding) {
9353
9757
  }
9354
9758
 
9355
9759
  // src/witan/generic-adapter.ts
9356
- import { readFileSync as readFileSync2 } from "fs";
9760
+ init_esm_shims();
9761
+ import { readFileSync } from "fs";
9357
9762
  var VALID_DIMENSIONS = /* @__PURE__ */ new Set([
9358
9763
  "A1",
9359
9764
  "A2",
@@ -9400,7 +9805,8 @@ function parseGenericJson(raw) {
9400
9805
  }
9401
9806
 
9402
9807
  // src/witan/sarif-adapter.ts
9403
- import { readFileSync as readFileSync3 } from "fs";
9808
+ init_esm_shims();
9809
+ import { readFileSync as readFileSync2 } from "fs";
9404
9810
  var DIMENSION_RULES = [
9405
9811
  // A2 — injection / secrets / auth (high weight)
9406
9812
  {
@@ -9545,7 +9951,8 @@ function parseSarifJson(raw, extraDimensionRules) {
9545
9951
  }
9546
9952
 
9547
9953
  // src/witan/scorecard-adapter.ts
9548
- import { readFileSync as readFileSync4 } from "fs";
9954
+ init_esm_shims();
9955
+ import { readFileSync as readFileSync3 } from "fs";
9549
9956
  var CHECK_RULES = {
9550
9957
  // B2 — PR outcome traceability
9551
9958
  "Branch-Protection": { dimension: "B2", weight: 0.8 },
@@ -9654,10 +10061,18 @@ function rawToolName(raw) {
9654
10061
  if (typeof obj.tool === "string" && obj.tool.length > 0) return obj.tool;
9655
10062
  return "scorecard";
9656
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
+ }
9657
10072
  function parseIngestFile(filePath, options = {}) {
9658
10073
  let raw;
9659
10074
  try {
9660
- raw = JSON.parse(stripBom(readFileSync5(filePath, "utf8")));
10075
+ raw = JSON.parse(stripBom(readFileSync4(filePath, "utf8")));
9661
10076
  } catch (error) {
9662
10077
  const message = error instanceof Error ? error.message : String(error);
9663
10078
  throw new Error(`Cejel: could not parse ingest file as JSON: ${filePath} (${message})`);
@@ -9681,6 +10096,8 @@ function parseIngestFile(filePath, options = {}) {
9681
10096
  }
9682
10097
  return signals.map((signal) => ({
9683
10098
  ...signal,
10099
+ source: sanitizeIngestSource(signal.source),
10100
+ provenance: options.provenance ?? "operator_supplied",
9684
10101
  findings: signal.findings.map(clampFinding)
9685
10102
  }));
9686
10103
  }
@@ -9689,7 +10106,7 @@ function expandIngestPattern(pattern) {
9689
10106
  const lastSlash = pattern.lastIndexOf("/");
9690
10107
  const dir = lastSlash === -1 ? "." : pattern.slice(0, lastSlash);
9691
10108
  const filePattern = lastSlash === -1 ? pattern : pattern.slice(lastSlash + 1);
9692
- const regexSource = filePattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
10109
+ const regexSource = filePattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
9693
10110
  const regex = new RegExp(`^${regexSource}$`);
9694
10111
  let entries;
9695
10112
  try {
@@ -9710,7 +10127,11 @@ function discoverIngestInputs(repoPath) {
9710
10127
  return entries.filter((name) => /\.(sarif|json)$/i.test(name)).sort().map((name) => join2(inputsDir, name));
9711
10128
  }
9712
10129
 
10130
+ // src/witan/scoring.ts
10131
+ init_esm_shims();
10132
+
9713
10133
  // src/witan/rubric.ts
10134
+ init_esm_shims();
9714
10135
  var WITAN_RUBRIC = [
9715
10136
  {
9716
10137
  id: "A1",
@@ -9777,6 +10198,9 @@ var MAX_SIGNAL_ADJUSTMENT = 0.8;
9777
10198
  function createWitanReport(input, inputSignals, rubric = WITAN_RUBRIC) {
9778
10199
  const parsedInput = WitanReportInputSchema.parse(input);
9779
10200
  const signalsByCriterion = mergeSignalsByCriterion(parsedInput.signals);
10201
+ const readFailureAbstentions = new Set(
10202
+ [...signalsByCriterion.values()].filter((signal) => signal.insufficientData === true).map((signal) => signal.criterionId)
10203
+ );
9780
10204
  const hasInputSignals = inputSignals != null && inputSignals.length > 0;
9781
10205
  const inputSignalsByDimension = /* @__PURE__ */ new Map();
9782
10206
  if (hasInputSignals && inputSignals) {
@@ -9840,7 +10264,8 @@ function createWitanReport(input, inputSignals, rubric = WITAN_RUBRIC) {
9840
10264
  const categoryScoreMap = {};
9841
10265
  for (const category of categoryOrder) {
9842
10266
  categoryScoreMap[category] = averageScore(
9843
- criteria.filter((criterion) => criterion.category === category)
10267
+ criteria.filter((criterion) => criterion.category === category),
10268
+ readFailureAbstentions
9844
10269
  );
9845
10270
  }
9846
10271
  const firstCategory = categoryOrder[0];
@@ -9863,7 +10288,6 @@ function createWitanReport(input, inputSignals, rubric = WITAN_RUBRIC) {
9863
10288
  productSlug: parsedInput.productSlug,
9864
10289
  productDisplayName: parsedInput.productDisplayName,
9865
10290
  repo: parsedInput.repo,
9866
- generatedAt: parsedInput.generatedAt,
9867
10291
  rubricVersion: parsedInput.rubricVersion,
9868
10292
  verdict: abstained ? "insufficient_source" : witanVerdictForScore(overallScore),
9869
10293
  codeTrustScore: abstained ? null : codeTrustScore,
@@ -9871,6 +10295,8 @@ function createWitanReport(input, inputSignals, rubric = WITAN_RUBRIC) {
9871
10295
  overallScore: abstained ? null : overallScore,
9872
10296
  criteria,
9873
10297
  ...consumedSignals.length > 0 ? { consumedSignals } : {},
10298
+ ...parsedInput.scanLimitations.length > 0 ? { scanLimitations: parsedInput.scanLimitations } : {},
10299
+ ...parsedInput.contentReadSummary ? { contentReadSummary: parsedInput.contentReadSummary } : {},
9874
10300
  // Only surface the full per-category map for rubrics with more than two buckets —
9875
10301
  // the default two-category rubric is fully represented by codeTrustScore/processTrustScore.
9876
10302
  ...!abstained && categoryOrder.length > 2 ? { categoryScores: categoryScoreMap } : {},
@@ -9896,6 +10322,7 @@ function applySignalAdjustment(nativeScore, dimension, signals) {
9896
10322
  totalRawAdj += signalAdj;
9897
10323
  summaries.push({
9898
10324
  source: signal.source,
10325
+ provenance: signal.provenance ?? "operator_supplied",
9899
10326
  dimension,
9900
10327
  findingCount: signal.findings.length,
9901
10328
  severityBreakdown: { critical: criticalCount, warning: warningCount, info: infoCount },
@@ -9961,8 +10388,9 @@ function mergeSignalsByCriterion(signals) {
9961
10388
  findings: [...existing.findings, ...signal.findings],
9962
10389
  metrics: [...existing.metrics ?? [], ...signal.metrics ?? []],
9963
10390
  notes: signal.notes ?? existing.notes,
9964
- // N/A wins on merge: if either signal is N/A, the criterion is N/A.
9965
- ...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 } : {}
9966
10394
  });
9967
10395
  }
9968
10396
  return merged;
@@ -9983,6 +10411,15 @@ function scoreCriterion(signal, rubricVersion) {
9983
10411
  metrics: []
9984
10412
  };
9985
10413
  }
10414
+ if (signal.insufficientData === true) {
10415
+ return {
10416
+ score: 0,
10417
+ status: unmeasuredStatus(rubricVersion),
10418
+ evidence: [],
10419
+ findings: [],
10420
+ metrics: []
10421
+ };
10422
+ }
9986
10423
  if (signal.notApplicable === true) {
9987
10424
  return {
9988
10425
  score: 0,
@@ -10358,9 +10795,9 @@ function ensureFindingsExplainStatus(criterionId, score, status, findings, evide
10358
10795
  }
10359
10796
  ];
10360
10797
  }
10361
- function averageScore(criteria) {
10798
+ function averageScore(criteria, readFailureAbstentions = /* @__PURE__ */ new Set()) {
10362
10799
  const applicable = criteria.filter(
10363
- (criterion) => criterion.status !== "not_applicable" && criterion.status !== "insufficient_data"
10800
+ (criterion) => criterion.status !== "not_applicable" && (criterion.status !== "insufficient_data" || readFailureAbstentions.has(criterion.id))
10364
10801
  );
10365
10802
  if (applicable.length === 0) return 0;
10366
10803
  const total = applicable.reduce((sum, criterion) => sum + criterion.score, 0);
@@ -10395,7 +10832,7 @@ function scoreRepoWithPublicCejel(options) {
10395
10832
  }
10396
10833
  function resolvePublicIngestSignals(options) {
10397
10834
  const seen = /* @__PURE__ */ new Set();
10398
- const files = [];
10835
+ const explicitFiles = [];
10399
10836
  for (const pattern of options.ingestPatterns ?? []) {
10400
10837
  const matches = expandIngestPattern(pattern);
10401
10838
  if (matches.length === 0 && options.warnOnEmptyIngestMatch) {
@@ -10406,20 +10843,28 @@ function resolvePublicIngestSignals(options) {
10406
10843
  const resolved = resolve2(match);
10407
10844
  if (seen.has(resolved)) continue;
10408
10845
  seen.add(resolved);
10409
- files.push(match);
10846
+ explicitFiles.push(match);
10410
10847
  }
10411
10848
  }
10849
+ const signals = explicitFiles.flatMap(
10850
+ (file) => parseIngestFile(file, { provenance: "operator_supplied" })
10851
+ );
10852
+ if (!options.autoDiscoverIngest) return signals;
10412
10853
  for (const discovered of discoverIngestInputs(options.repoPath)) {
10413
10854
  const resolved = resolve2(discovered);
10414
10855
  if (seen.has(resolved)) continue;
10415
10856
  seen.add(resolved);
10416
- files.push(discovered);
10857
+ signals.push(...parseIngestFile(discovered, { provenance: "auto_discovered" }));
10417
10858
  }
10418
- return files.flatMap((file) => parseIngestFile(file));
10859
+ return signals;
10419
10860
  }
10420
10861
 
10862
+ // src/scan.ts
10863
+ init_esm_shims();
10864
+
10421
10865
  // src/product-identity.ts
10422
- import { existsSync, readFileSync as readFileSync6 } from "fs";
10866
+ init_esm_shims();
10867
+ import { existsSync, readFileSync as readFileSync5 } from "fs";
10423
10868
  import { basename as basename2, join as join3 } from "path";
10424
10869
  var SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$/;
10425
10870
  var FALLBACK_SLUG = "repo";
@@ -10436,7 +10881,7 @@ function deriveProductIdentity(repoPath) {
10436
10881
  function readPackageName(packageJsonPath) {
10437
10882
  if (!existsSync(packageJsonPath)) return void 0;
10438
10883
  try {
10439
- const parsed = JSON.parse(readFileSync6(packageJsonPath, "utf8"));
10884
+ const parsed = JSON.parse(readFileSync5(packageJsonPath, "utf8"));
10440
10885
  if (typeof parsed === "object" && parsed !== null && "name" in parsed) {
10441
10886
  const name = parsed.name;
10442
10887
  if (typeof name === "string" && name.trim().length > 0) return name;
@@ -10453,7 +10898,11 @@ function slugify(raw) {
10453
10898
  return FALLBACK_SLUG;
10454
10899
  }
10455
10900
 
10901
+ // src/summary.ts
10902
+ init_esm_shims();
10903
+
10456
10904
  // src/finding-presentation.ts
10905
+ init_esm_shims();
10457
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\.$/;
10458
10907
  var REMEDIATION_BY_METRIC2 = {
10459
10908
  test_to_source_ratio: "add concrete regression tests for the implementation surface",
@@ -10565,13 +11014,14 @@ function buildWitanCliSummary(report) {
10565
11014
  const common = {
10566
11015
  productSlug: report.productSlug,
10567
11016
  productDisplayName: report.productDisplayName,
10568
- generatedAt: report.generatedAt,
10569
11017
  findingCount: allFindings.length,
10570
11018
  topFindings: sorted.slice(0, TOP_FINDINGS_LIMIT),
10571
11019
  contributingSources,
10572
11020
  externalSources,
10573
11021
  externalFindingCount: allExternalFindings.length,
10574
- 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 } : {}
10575
11025
  };
10576
11026
  if (report.verdict === "insufficient_source") {
10577
11027
  return {
@@ -10595,21 +11045,24 @@ function buildWitanCliSummary(report) {
10595
11045
  // src/scan.ts
10596
11046
  function runCejelScan(options) {
10597
11047
  const identity = deriveProductIdentity(options.repoPath);
11048
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
10598
11049
  const report = scoreRepoWithPublicCejel({
10599
11050
  productSlug: identity.productSlug,
10600
11051
  productDisplayName: options.productDisplayName ?? identity.productDisplayName,
10601
11052
  repoPath: options.repoPath,
11053
+ generatedAt,
10602
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,
10603
11059
  warnOnEmptyIngestMatch: options.warnOnEmptyIngestMatch
10604
11060
  });
10605
11061
  const summary = buildWitanCliSummary(report);
10606
- return { report, summary };
11062
+ return { report, summary, generatedAt };
10607
11063
  }
10608
11064
 
10609
11065
  export {
10610
- __commonJS,
10611
- __export,
10612
- __toESM,
10613
11066
  ZodOptional,
10614
11067
  ZodFirstPartyTypeKind,
10615
11068
  objectType,