@cejel/cejel 0.6.7 → 0.6.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,16 +5,17 @@ import {
5
5
  describeGitFailure,
6
6
  execGit,
7
7
  isExpectedGitAbsence
8
- } from "./chunk-VGBFZBZQ.js";
8
+ } from "./chunk-NIWSYJ6N.js";
9
9
  import {
10
10
  external_exports
11
- } from "./chunk-FSMZV3Y6.js";
11
+ } from "./chunk-AMVP7L26.js";
12
12
  import {
13
13
  MAX_REPOSITORY_CONTENT_BYTES,
14
14
  abstainSignalOnWithheldPaths,
15
15
  classifyAbstentionReason,
16
16
  contentReadSignalKey,
17
17
  init_esm_shims,
18
+ isGitLfsPointerText,
18
19
  readRepoText,
19
20
  readRepoTextPrefix,
20
21
  recordContentSkip,
@@ -24,8 +25,9 @@ import {
24
25
  withContentReadCriterion,
25
26
  withContentReadSignal,
26
27
  withContentReadSignals,
27
- withheldRepoPaths
28
- } from "./chunk-O3MHSK25.js";
28
+ withheldRepoPaths,
29
+ withoutContentReadAttribution
30
+ } from "./chunk-AWLKQUI4.js";
29
31
 
30
32
  // src/witan/schemas.ts
31
33
  init_esm_shims();
@@ -174,14 +176,18 @@ var WitanContentReadSkipReasonSchema = external_exports.enum([
174
176
  "too_large",
175
177
  "excluded_by_extension",
176
178
  "denied_path",
177
- "non_regular_file"
179
+ "non_regular_file",
180
+ // Additive, report format 1.6 (#519): the checkout held a Git LFS pointer in place of content.
181
+ "lfs_pointer"
178
182
  ]);
183
+ var WitanCriterionDisclosuresSchema = external_exports.array(external_exports.string().min(1).max(1e3)).min(1).max(4);
179
184
  var WitanCriterionSignalSchema = external_exports.object({
180
185
  criterionId: WitanCriterionIdSchema,
181
186
  positiveEvidence: external_exports.array(WitanEvidencePointerSchema).default([]),
182
187
  findings: external_exports.array(WitanFindingSchema).default([]),
183
188
  metrics: external_exports.array(WitanCriterionMetricSchema).default([]),
184
189
  notes: external_exports.string().min(1).max(1e3).optional(),
190
+ disclosures: WitanCriterionDisclosuresSchema.optional(),
185
191
  notApplicable: external_exports.literal(true).optional(),
186
192
  // A collector reached relevant files but could not read their contents. This is a
187
193
  // measurement gap, never negative evidence: scoreCriterion maps it to insufficient_data.
@@ -206,12 +212,18 @@ var WitanContentReadSummarySchema = external_exports.object({
206
212
  tooLarge: external_exports.number().int().nonnegative(),
207
213
  excludedByExtension: external_exports.number().int().nonnegative(),
208
214
  deniedPath: external_exports.number().int().nonnegative(),
209
- nonRegularFile: external_exports.number().int().nonnegative()
215
+ nonRegularFile: external_exports.number().int().nonnegative(),
216
+ // Additive-optional, report format 1.6 (#519): files whose checkout held a Git LFS pointer
217
+ // in place of their content. Present only when non-zero.
218
+ lfsPointer: external_exports.number().int().positive().optional()
210
219
  }).strict(),
211
220
  unreadableByErrno: external_exports.record(external_exports.string().min(1).max(32), external_exports.number().int().positive()),
212
221
  affectedCriteria: external_exports.array(WitanCriterionIdSchema)
213
222
  }).strict().superRefine((summary, context) => {
214
- const reasonTotal = Object.values(summary.byReason).reduce((sum, count) => sum + count, 0);
223
+ const reasonTotal = Object.values(summary.byReason).reduce(
224
+ (sum, count) => sum + (count ?? 0),
225
+ 0
226
+ );
215
227
  if (reasonTotal !== summary.skipped) {
216
228
  context.addIssue({
217
229
  code: external_exports.ZodIssueCode.custom,
@@ -266,11 +278,17 @@ var WitanCriterionScoreSchema = external_exports.object({
266
278
  category: WitanCriterionCategorySchema,
267
279
  score: external_exports.number().min(0).max(4),
268
280
  nativeScore: external_exports.number().min(0).max(4).optional(),
281
+ // Additive-optional (report format 1.7): the status the native evidence alone produced, set
282
+ // beside nativeScore whenever ingested signals adjusted this criterion. When it differs from
283
+ // `status`, imported evidence moved the status; a reader must not take the moved status as a
284
+ // native Cejel finding. Absent when no ingested signal adjusted the criterion.
285
+ nativeStatus: WitanCriterionStatusSchema.optional(),
269
286
  status: WitanCriterionStatusSchema,
270
287
  evidence: external_exports.array(WitanEvidencePointerSchema),
271
288
  findings: external_exports.array(WitanFindingSchema),
272
289
  metrics: external_exports.array(WitanCriterionMetricSchema).default([]),
273
- notes: external_exports.string().min(1).max(1e3).optional()
290
+ notes: external_exports.string().min(1).max(1e3).optional(),
291
+ disclosures: WitanCriterionDisclosuresSchema.optional()
274
292
  }).strict();
275
293
  var WitanInputSignalFindingSchema = external_exports.object({
276
294
  ruleId: external_exports.string().min(1).max(200),
@@ -309,6 +327,62 @@ var WitanConsumedSignalSummarySchema = external_exports.object({
309
327
  // this field always carries every finding.
310
328
  findings: external_exports.array(WitanInputSignalFindingSchema).default([])
311
329
  }).strict();
330
+ var WitanInputBindingStatusSchema = external_exports.enum([
331
+ "matched",
332
+ "mismatched-repository",
333
+ "mismatched-commit",
334
+ "stale",
335
+ "unbound",
336
+ "unverifiable"
337
+ ]);
338
+ var WitanInputBindingCheckSchema = external_exports.enum(["matched", "mismatched", "absent", "unverifiable"]);
339
+ var WitanInputBindingRecordSchema = external_exports.object({
340
+ sha256: external_exports.string().regex(/^[a-f0-9]{64}$/),
341
+ format: external_exports.enum(["sarif", "scorecard", "generic"]),
342
+ provenance: WitanIngestProvenanceSchema,
343
+ producers: external_exports.array(
344
+ external_exports.object({
345
+ name: external_exports.string().min(1).max(120),
346
+ version: external_exports.string().min(1).max(80).optional(),
347
+ commit: external_exports.string().min(1).max(80).optional()
348
+ }).strict()
349
+ ).max(8),
350
+ subjects: external_exports.array(
351
+ external_exports.object({
352
+ repository: external_exports.string().min(1).max(300).optional(),
353
+ // Additive-optional (1.7, #525 follow-up): the input declared a repository Cejel could
354
+ // not normalize to host/path. The declared text is withheld, never recorded, because
355
+ // it may carry URL credentials. Never treated as the named subject repository.
356
+ repositoryWithheld: external_exports.literal(true).optional(),
357
+ commit: external_exports.string().min(1).max(80).optional()
358
+ }).strict()
359
+ ).max(8),
360
+ // Additive-optional (1.7, #525 follow-up): distinct declared subjects beyond the 8 recorded.
361
+ // Every declared subject is checked; only the record is capped. Absent when none were omitted.
362
+ // Bounded by the 8 MiB ingest document budget: each distinct subject takes at least one byte.
363
+ subjectsOmitted: external_exports.number().int().min(1).max(8 * 1024 * 1024).optional(),
364
+ observedAt: external_exports.string().datetime().optional(),
365
+ status: WitanInputBindingStatusSchema,
366
+ checks: external_exports.object({
367
+ repository: WitanInputBindingCheckSchema,
368
+ commit: WitanInputBindingCheckSchema,
369
+ freshness: external_exports.enum(["within-window", "stale", "absent", "unverifiable"])
370
+ }).strict(),
371
+ duplicate: external_exports.enum(["same-path", "same-content"]).optional(),
372
+ // False only for a same-path duplicate, which the ingest funnel has always skipped. A
373
+ // same-content duplicate at a different path is scored, exactly as before binding existed.
374
+ scored: external_exports.boolean()
375
+ }).strict();
376
+ var WitanInputBindingSchema = external_exports.object({
377
+ policy: external_exports.enum(["record-only", "strict"]),
378
+ freshnessWindowSeconds: external_exports.number().int().min(0),
379
+ scannedSubject: external_exports.object({
380
+ repository: external_exports.string().min(1).max(300).optional(),
381
+ commit: external_exports.string().min(7).max(64).optional(),
382
+ commitTime: external_exports.string().datetime().optional()
383
+ }).strict(),
384
+ inputs: external_exports.array(WitanInputBindingRecordSchema).max(256)
385
+ }).strict();
312
386
  var WitanScoredVerdictSchema = external_exports.enum([
313
387
  "verified",
314
388
  "conditional",
@@ -343,6 +417,9 @@ var WitanReportCommonSchema = external_exports.object({
343
417
  // Rubric-driven: length matches whatever rubric produced this report, not a fixed enum.
344
418
  criteria: external_exports.array(WitanCriterionScoreSchema).min(1),
345
419
  consumedSignals: external_exports.array(WitanConsumedSignalSummarySchema).optional(),
420
+ // Additive-optional (report format 1.7): present only when at least one document was ingested,
421
+ // so a no-ingest report stays byte-identical. See WitanInputBindingSchema.
422
+ inputBinding: WitanInputBindingSchema.optional(),
346
423
  archetype: WitanRepoArchetypeSchema.optional(),
347
424
  scanLimitations: external_exports.array(external_exports.string().min(1).max(1e3)).max(16).optional(),
348
425
  // Additive-optional (report format 1.5, issue #428): disclosures about the commit history the
@@ -401,7 +478,7 @@ var WitanReportSchema = external_exports.preprocess((value) => {
401
478
  }, StrictWitanReportSchema);
402
479
  var WITAN_ATTESTATION_STATEMENT_TYPE = "https://in-toto.io/Statement/v1";
403
480
  var WITAN_ATTESTATION_PREDICATE_TYPE = "https://cejel.dev/attestations/scan/v1";
404
- var WITAN_REPORT_FORMAT_VERSION = "1.5";
481
+ var WITAN_REPORT_FORMAT_VERSION = "1.8";
405
482
  var WitanAttestationOutcomeSchema = external_exports.discriminatedUnion("status", [
406
483
  external_exports.object({
407
484
  status: external_exports.literal("scored"),
@@ -534,6 +611,7 @@ var WITAN_RUBRIC_VERSION_V23 = "witan-rubric-v23-prospective-2026-09-06";
534
611
  var WITAN_RUBRIC_VERSION_V24 = "witan-rubric-v24-prospective-2026-09-15";
535
612
  var WITAN_RUBRIC_VERSION_V25 = "witan-rubric-v25-prospective-2026-10-12";
536
613
  var WITAN_RUBRIC_VERSION_V26 = "witan-rubric-v26-prospective-2026-10-14";
614
+ var WITAN_RUBRIC_VERSION_V27 = "witan-rubric-v27-prospective-2026-10-19";
537
615
  var WITAN_LAST_CALIBRATED_RUBRIC_VERSION = WITAN_RUBRIC_VERSION_V17;
538
616
  var WITAN_PROSPECTIVE_RUBRIC_VERSIONS = Object.freeze([
539
617
  WITAN_RUBRIC_VERSION_V18,
@@ -544,7 +622,8 @@ var WITAN_PROSPECTIVE_RUBRIC_VERSIONS = Object.freeze([
544
622
  WITAN_RUBRIC_VERSION_V23,
545
623
  WITAN_RUBRIC_VERSION_V24,
546
624
  WITAN_RUBRIC_VERSION_V25,
547
- WITAN_RUBRIC_VERSION_V26
625
+ WITAN_RUBRIC_VERSION_V26,
626
+ WITAN_RUBRIC_VERSION_V27
548
627
  ]);
549
628
  var WITAN_SELECTABLE_RUBRIC_VERSIONS = Object.freeze([
550
629
  WITAN_LAST_CALIBRATED_RUBRIC_VERSION,
@@ -573,6 +652,8 @@ var WITAN_LEGACY_AUTHENTICATED_A1_ABSENCE_SUMMARY = "Reviewable source is presen
573
652
  var WITAN_A5_RECONCILIATION_UNSETTLED_DISCLOSURE = "Whether this repository's claims are reconciled against its implementation cannot be settled from static evidence; Cejel makes no finding on it.";
574
653
  var WITAN_A2_TENANT_ISOLATION_UNSETTLED_DISCLOSURE = "Whether tenant isolation is enforced outside the database cannot be settled from static evidence; Cejel makes no finding on it.";
575
654
  var WITAN_A3_BUILD_STEP_UNSETTLED_DISCLOSURE = "Whether this package has a build or type-check step outside the files read cannot be settled from static evidence; Cejel makes no finding on it.";
655
+ var WITAN_A2_PLACEHOLDER_SET_ASIDE_PREFIX = "Cejel read and set aside ";
656
+ var WITAN_B3_UNSHOWN_CHECK_PREFIX = "B3 counted workflows Cejel did not see run a check: ";
576
657
  function isWitanNoMeasurementAbstention(report) {
577
658
  return report.verdict === "insufficient_source" && report.insufficientSourceReason.startsWith(WITAN_NO_MEASUREMENT_REASON) && report.criteria.every(
578
659
  ({ status }) => status === "not_applicable" || status === "insufficient_data"
@@ -636,6 +717,76 @@ function collectExternalFindings(consumedSignals) {
636
717
  });
637
718
  }
638
719
 
720
+ // src/witan/input-binding-presentation.ts
721
+ init_esm_shims();
722
+ var INHERITED_FINDING_NOTES = Object.freeze({
723
+ "scorecard:Dependency-Update-Tool": "Restates evidence Cejel reads natively: A4 dependency_automation_ratio credits the same Dependabot or Renovate configuration. Not independent corroboration; for the same absence this adjusts A4 a second time.",
724
+ "scorecard:Security-Policy": "Restates evidence Cejel reads natively: B4 counts SECURITY.md as an audit-trail file. Not independent corroboration.",
725
+ "scorecard:SAST": "Scorecard SAST reports whether a static-analysis tool runs on commits. It is not a secrets or data-layer finding; the current ingest mapping folds it into A2."
726
+ });
727
+ function inheritedFindingNote(ruleId) {
728
+ return Object.hasOwn(INHERITED_FINDING_NOTES, ruleId) ? INHERITED_FINDING_NOTES[ruleId] : void 0;
729
+ }
730
+ function formatScore(score) {
731
+ return score.toFixed(1);
732
+ }
733
+ function importedAdjustmentSentence(criterion) {
734
+ if (criterion.nativeScore === void 0) return void 0;
735
+ if (criterion.status === "not_applicable" || criterion.status === "insufficient_data") {
736
+ return `Ingested findings were folded into this dimension, but it is ${criterion.status}, so they moved no score.`;
737
+ }
738
+ const statusMoved = criterion.nativeStatus !== void 0 && criterion.nativeStatus !== criterion.status;
739
+ if (criterion.nativeScore === criterion.score) {
740
+ return statusMoved ? `Imported evidence moved this status: Cejel's own evidence gave ${criterion.nativeStatus} at ${formatScore(criterion.nativeScore)}/4.0; ingested findings did not move the score but moved the status to ${criterion.status}. The move is not a native Cejel finding.` : `Ingested findings were folded into this dimension but did not move its score from Cejel's own ${formatScore(criterion.nativeScore)}/4.0; the status is the native one.`;
741
+ }
742
+ const difference = Math.abs(criterion.nativeScore - criterion.score);
743
+ const delta = difference < 0.05 ? "less than 0.1" : formatScore(difference);
744
+ if (statusMoved) {
745
+ return `Imported evidence moved this status: Cejel's own evidence gave ${criterion.nativeStatus} at ${formatScore(criterion.nativeScore)}/4.0; ingested findings lowered the score by ${delta} to ${formatScore(criterion.score)}, which moved the status to ${criterion.status}. The move is not a native Cejel finding.`;
746
+ }
747
+ return `Ingested findings lowered this score by ${delta} from Cejel's own ${formatScore(criterion.nativeScore)}/4.0; the status is the native one.`;
748
+ }
749
+ var STATUS_ORDER = [
750
+ "matched",
751
+ "unbound",
752
+ "unverifiable",
753
+ "stale",
754
+ "mismatched-commit",
755
+ "mismatched-repository"
756
+ ];
757
+ function inputBindingHeadline(binding) {
758
+ const counts = /* @__PURE__ */ new Map();
759
+ for (const input of binding.inputs) counts.set(input.status, (counts.get(input.status) ?? 0) + 1);
760
+ const parts = STATUS_ORDER.filter((status) => counts.has(status)).map(
761
+ (status) => `${counts.get(status)} ${status}`
762
+ );
763
+ const duplicates = binding.inputs.filter((input) => input.duplicate !== void 0).length;
764
+ if (duplicates > 0) parts.push(`${duplicates} duplicate`);
765
+ const n = binding.inputs.length;
766
+ const policy = binding.policy === "strict" ? "strict: mismatched, stale and duplicate inputs were refused" : "record only: binding status did not affect any score";
767
+ return `${n} ingested input${n === 1 ? "" : "s"}: ${parts.join(", ")} (${policy}).`;
768
+ }
769
+ function describeSubject(record) {
770
+ if (record.subjects.length === 0) return "no declared subject";
771
+ const listed = record.subjects.map((subject) => {
772
+ const repository = subject.repository ?? (subject.repositoryWithheld ? "repository withheld (declared, not normalizable)" : "repository not declared");
773
+ return `${repository} @ ${subject.commit ?? "commit not declared"}`;
774
+ }).join("; ");
775
+ return record.subjectsOmitted ? `${listed}; and ${record.subjectsOmitted} more declared subject${record.subjectsOmitted === 1 ? "" : "s"}, all checked, not recorded` : listed;
776
+ }
777
+ function describeProducer(record) {
778
+ if (record.producers.length === 0) return record.format;
779
+ return record.producers.map((producer) => producer.version ? `${producer.name} ${producer.version}` : producer.name).join(", ");
780
+ }
781
+ function describeInputBindingRecord(record) {
782
+ const duplicate = record.duplicate ? `; duplicate (${record.duplicate}${record.scored ? ", scored again as before" : ", not scored again"})` : "";
783
+ return `${record.status}: ${describeProducer(record)} (${record.format}, ${record.provenance}); subject ${describeSubject(record)}; observed ${record.observedAt ?? "not declared"}; sha256:${record.sha256}${duplicate}`;
784
+ }
785
+ function describeScannedSubject(binding) {
786
+ const subject = binding.scannedSubject;
787
+ return `Compared with: ${subject.repository ?? "repository not named (pass --subject-repository)"} @ ${subject.commit ?? "no git HEAD"}${subject.commitTime ? `, committed ${subject.commitTime}` : ""}; stale means observed more than ${binding.freshnessWindowSeconds} seconds before that commit time.`;
788
+ }
789
+
639
790
  // src/witan/finding-presentation.ts
640
791
  init_esm_shims();
641
792
 
@@ -663,7 +814,11 @@ var WITAN_RUBRIC_BEHAVIOUR_FINGERPRINTS = Object.freeze({
663
814
  // fires on it and, by amendment 1, A5 is scored without reconciliation_artifact_depth. The A5
664
815
  // component and the report components of the specimens whose code-trust score that moves differ
665
816
  // from v25's. A2 does not move on this corpus.
666
- [WITAN_RUBRIC_VERSION_V26]: "sha256:4530317d56fc73eb89287e344ad626e2b9a96908ef12eb9e9c13b839bd599dae"
817
+ [WITAN_RUBRIC_VERSION_V26]: "sha256:4530317d56fc73eb89287e344ad626e2b9a96908ef12eb9e9c13b839bd599dae",
818
+ // v17's closure plus the v27 false-assurance gate (#517-#520, #442). On this corpus only A4
819
+ // moves against v17: specimens whose CI runs `npm ci` with no committed lockfile score
820
+ // lockfile_coverage 0 under library norms. No report-level component differs from v17's.
821
+ [WITAN_RUBRIC_VERSION_V27]: "sha256:fb4edfa0bdf0d8153af3f84a4b72d4eab6da3fd6e0b2e3e494cea9542ded45a0"
667
822
  });
668
823
  function rubricBehaviourFingerprint(rubricVersion) {
669
824
  return WITAN_RUBRIC_BEHAVIOUR_FINGERPRINTS[rubricVersion];
@@ -731,7 +886,7 @@ var WITAN_RUBRIC = [
731
886
 
732
887
  // src/witan/scoring.ts
733
888
  function usesV17DetectorClosure(rubricVersion) {
734
- return rubricVersion === WITAN_RUBRIC_VERSION_V17 || rubricVersion === WITAN_RUBRIC_VERSION_V18 || rubricVersion === WITAN_RUBRIC_VERSION_V19 || rubricVersion === WITAN_RUBRIC_VERSION_V20 || rubricVersion === WITAN_RUBRIC_VERSION_V21 || rubricVersion === WITAN_RUBRIC_VERSION_V22 || rubricVersion === WITAN_RUBRIC_VERSION_V25 || rubricVersion === WITAN_RUBRIC_VERSION_V26 || rubricVersion === WITAN_RUBRIC_VERSION_V24 || rubricVersion === WITAN_RUBRIC_VERSION_V23;
889
+ return rubricVersion === WITAN_RUBRIC_VERSION_V17 || rubricVersion === WITAN_RUBRIC_VERSION_V27 || rubricVersion === WITAN_RUBRIC_VERSION_V18 || rubricVersion === WITAN_RUBRIC_VERSION_V19 || rubricVersion === WITAN_RUBRIC_VERSION_V20 || rubricVersion === WITAN_RUBRIC_VERSION_V21 || rubricVersion === WITAN_RUBRIC_VERSION_V22 || rubricVersion === WITAN_RUBRIC_VERSION_V25 || rubricVersion === WITAN_RUBRIC_VERSION_V26 || rubricVersion === WITAN_RUBRIC_VERSION_V24 || rubricVersion === WITAN_RUBRIC_VERSION_V23;
735
890
  }
736
891
  var MAX_SIGNAL_ADJUSTMENT = 0.8;
737
892
  function createWitanReport(input, inputSignals, rubric = WITAN_RUBRIC, toolVersion) {
@@ -765,7 +920,8 @@ function createWitanReport(input, inputSignals, rubric = WITAN_RUBRIC, toolVersi
765
920
  evidence: scored.evidence,
766
921
  findings: scored.findings,
767
922
  metrics: scored.metrics,
768
- ...signal?.notes ? { notes: signal.notes } : {}
923
+ ...signal?.notes ? { notes: signal.notes } : {},
924
+ ...signal?.disclosures ? { disclosures: signal.disclosures } : {}
769
925
  };
770
926
  }
771
927
  const { adjustedScore, adjustment, summaries } = applySignalAdjustment(
@@ -787,11 +943,16 @@ function createWitanReport(input, inputSignals, rubric = WITAN_RUBRIC, toolVersi
787
943
  category: rubricCriterion.category,
788
944
  score: finalScore,
789
945
  ...adjustment !== 0 ? { nativeScore: scored.score } : {},
946
+ // Beside nativeScore, so a reader can tell a status that imported evidence moved from one
947
+ // the native evidence produced (report format 1.7). Presentation-only: finalStatus above is
948
+ // computed exactly as before.
949
+ ...adjustment !== 0 ? { nativeStatus: scored.status } : {},
790
950
  status: finalStatus,
791
951
  evidence: scored.evidence,
792
952
  findings: scored.findings,
793
953
  metrics: scored.metrics,
794
- ...signal?.notes ? { notes: signal.notes } : {}
954
+ ...signal?.notes ? { notes: signal.notes } : {},
955
+ ...signal?.disclosures ? { disclosures: signal.disclosures } : {}
795
956
  };
796
957
  });
797
958
  const categoryOrder = [];
@@ -902,7 +1063,7 @@ function statusForScore(score) {
902
1063
  }
903
1064
  function statusAfterInputAdjustment(nativeStatus, nativeScore, adjustment, rubricVersion) {
904
1065
  if (adjustment === 0) return nativeStatus;
905
- if (rubricVersion !== WITAN_RUBRIC_VERSION_V8 && rubricVersion !== WITAN_RUBRIC_VERSION_V9 && rubricVersion !== WITAN_RUBRIC_VERSION_V10 && rubricVersion !== WITAN_RUBRIC_VERSION_V11 && rubricVersion !== WITAN_RUBRIC_VERSION_V12 && rubricVersion !== WITAN_RUBRIC_VERSION_V13 && rubricVersion !== WITAN_RUBRIC_VERSION_V14 && rubricVersion !== WITAN_RUBRIC_VERSION_V15 && rubricVersion !== WITAN_RUBRIC_VERSION_V16 && rubricVersion !== WITAN_RUBRIC_VERSION_V17 && rubricVersion !== WITAN_RUBRIC_VERSION_V18 && rubricVersion !== WITAN_RUBRIC_VERSION_V19 && rubricVersion !== WITAN_RUBRIC_VERSION_V20 && rubricVersion !== WITAN_RUBRIC_VERSION_V21 && rubricVersion !== WITAN_RUBRIC_VERSION_V22 && rubricVersion !== WITAN_RUBRIC_VERSION_V25 && rubricVersion !== WITAN_RUBRIC_VERSION_V26 && rubricVersion !== WITAN_RUBRIC_VERSION_V24 && rubricVersion !== WITAN_RUBRIC_VERSION_V23) {
1066
+ if (rubricVersion !== WITAN_RUBRIC_VERSION_V8 && rubricVersion !== WITAN_RUBRIC_VERSION_V9 && rubricVersion !== WITAN_RUBRIC_VERSION_V10 && rubricVersion !== WITAN_RUBRIC_VERSION_V11 && rubricVersion !== WITAN_RUBRIC_VERSION_V12 && rubricVersion !== WITAN_RUBRIC_VERSION_V13 && rubricVersion !== WITAN_RUBRIC_VERSION_V14 && rubricVersion !== WITAN_RUBRIC_VERSION_V15 && rubricVersion !== WITAN_RUBRIC_VERSION_V16 && rubricVersion !== WITAN_RUBRIC_VERSION_V17 && rubricVersion !== WITAN_RUBRIC_VERSION_V27 && rubricVersion !== WITAN_RUBRIC_VERSION_V18 && rubricVersion !== WITAN_RUBRIC_VERSION_V19 && rubricVersion !== WITAN_RUBRIC_VERSION_V20 && rubricVersion !== WITAN_RUBRIC_VERSION_V21 && rubricVersion !== WITAN_RUBRIC_VERSION_V22 && rubricVersion !== WITAN_RUBRIC_VERSION_V25 && rubricVersion !== WITAN_RUBRIC_VERSION_V26 && rubricVersion !== WITAN_RUBRIC_VERSION_V24 && rubricVersion !== WITAN_RUBRIC_VERSION_V23) {
906
1067
  return statusForScore(roundScore(Math.max(0, nativeScore - adjustment)));
907
1068
  }
908
1069
  const statusFloor = {
@@ -950,6 +1111,7 @@ function mergeSignalsByCriterion(signals) {
950
1111
  findings: [...existing.findings, ...signal.findings],
951
1112
  metrics: [...existing.metrics ?? [], ...signal.metrics ?? []],
952
1113
  notes: signal.notes ?? existing.notes,
1114
+ ...mergedDisclosures(existing.disclosures, signal.disclosures),
953
1115
  ...existingAbstains || signalAbstains ? {
954
1116
  insufficientData: true,
955
1117
  ...abstentionReason !== void 0 ? { insufficientDataReason: abstentionReason } : {}
@@ -958,8 +1120,12 @@ function mergeSignalsByCriterion(signals) {
958
1120
  }
959
1121
  return merged;
960
1122
  }
1123
+ function mergedDisclosures(existing, incoming) {
1124
+ const disclosures = [.../* @__PURE__ */ new Set([...existing ?? [], ...incoming ?? []])].slice(0, 4);
1125
+ return disclosures.length > 0 ? { disclosures } : {};
1126
+ }
961
1127
  function usesMetricScoring(rubricVersion) {
962
- return rubricVersion === WITAN_RUBRIC_VERSION_V1 || rubricVersion === WITAN_RUBRIC_VERSION_V2 || rubricVersion === WITAN_RUBRIC_VERSION_V3 || rubricVersion === WITAN_RUBRIC_VERSION_V4 || rubricVersion === WITAN_RUBRIC_VERSION_V5 || rubricVersion === WITAN_RUBRIC_VERSION_V6 || rubricVersion === WITAN_RUBRIC_VERSION_V7 || rubricVersion === WITAN_RUBRIC_VERSION_V8 || rubricVersion === WITAN_RUBRIC_VERSION_V9 || rubricVersion === WITAN_RUBRIC_VERSION_V10 || rubricVersion === WITAN_RUBRIC_VERSION_V11 || rubricVersion === WITAN_RUBRIC_VERSION_V12 || rubricVersion === WITAN_RUBRIC_VERSION_V13 || rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15 || rubricVersion === WITAN_RUBRIC_VERSION_V16 || rubricVersion === WITAN_RUBRIC_VERSION_V17 || rubricVersion === WITAN_RUBRIC_VERSION_V18 || rubricVersion === WITAN_RUBRIC_VERSION_V19 || rubricVersion === WITAN_RUBRIC_VERSION_V20 || rubricVersion === WITAN_RUBRIC_VERSION_V21 || rubricVersion === WITAN_RUBRIC_VERSION_V22 || rubricVersion === WITAN_RUBRIC_VERSION_V25 || rubricVersion === WITAN_RUBRIC_VERSION_V26 || rubricVersion === WITAN_RUBRIC_VERSION_V24 || rubricVersion === WITAN_RUBRIC_VERSION_V23 || rubricVersion === WITAN_TRADING_RUBRIC_VERSION_V0;
1128
+ return rubricVersion === WITAN_RUBRIC_VERSION_V1 || rubricVersion === WITAN_RUBRIC_VERSION_V2 || rubricVersion === WITAN_RUBRIC_VERSION_V3 || rubricVersion === WITAN_RUBRIC_VERSION_V4 || rubricVersion === WITAN_RUBRIC_VERSION_V5 || rubricVersion === WITAN_RUBRIC_VERSION_V6 || rubricVersion === WITAN_RUBRIC_VERSION_V7 || rubricVersion === WITAN_RUBRIC_VERSION_V8 || rubricVersion === WITAN_RUBRIC_VERSION_V9 || rubricVersion === WITAN_RUBRIC_VERSION_V10 || rubricVersion === WITAN_RUBRIC_VERSION_V11 || rubricVersion === WITAN_RUBRIC_VERSION_V12 || rubricVersion === WITAN_RUBRIC_VERSION_V13 || rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15 || rubricVersion === WITAN_RUBRIC_VERSION_V16 || rubricVersion === WITAN_RUBRIC_VERSION_V17 || rubricVersion === WITAN_RUBRIC_VERSION_V27 || rubricVersion === WITAN_RUBRIC_VERSION_V18 || rubricVersion === WITAN_RUBRIC_VERSION_V19 || rubricVersion === WITAN_RUBRIC_VERSION_V20 || rubricVersion === WITAN_RUBRIC_VERSION_V21 || rubricVersion === WITAN_RUBRIC_VERSION_V22 || rubricVersion === WITAN_RUBRIC_VERSION_V25 || rubricVersion === WITAN_RUBRIC_VERSION_V26 || rubricVersion === WITAN_RUBRIC_VERSION_V24 || rubricVersion === WITAN_RUBRIC_VERSION_V23 || rubricVersion === WITAN_TRADING_RUBRIC_VERSION_V0;
963
1129
  }
964
1130
  function unmeasuredStatus(rubricVersion) {
965
1131
  return rubricVersion === WITAN_TRADING_RUBRIC_VERSION_V0 ? "unverified" : "insufficient_data";
@@ -1347,7 +1513,7 @@ function calibratedCriterionStateV11(criterionId, metrics) {
1347
1513
  return void 0;
1348
1514
  }
1349
1515
  function ensureFindingsExplainStatus(criterionId, score, status, findings, evidence, rubricVersion) {
1350
- if (rubricVersion !== WITAN_RUBRIC_VERSION_V8 && rubricVersion !== WITAN_RUBRIC_VERSION_V9 && rubricVersion !== WITAN_RUBRIC_VERSION_V10 && rubricVersion !== WITAN_RUBRIC_VERSION_V11 && rubricVersion !== WITAN_RUBRIC_VERSION_V12 && rubricVersion !== WITAN_RUBRIC_VERSION_V13 && rubricVersion !== WITAN_RUBRIC_VERSION_V14 && rubricVersion !== WITAN_RUBRIC_VERSION_V15 && rubricVersion !== WITAN_RUBRIC_VERSION_V16 && rubricVersion !== WITAN_RUBRIC_VERSION_V17 && rubricVersion !== WITAN_RUBRIC_VERSION_V18 && rubricVersion !== WITAN_RUBRIC_VERSION_V19 && rubricVersion !== WITAN_RUBRIC_VERSION_V20 && rubricVersion !== WITAN_RUBRIC_VERSION_V21 && rubricVersion !== WITAN_RUBRIC_VERSION_V22 && rubricVersion !== WITAN_RUBRIC_VERSION_V25 && rubricVersion !== WITAN_RUBRIC_VERSION_V26 && rubricVersion !== WITAN_RUBRIC_VERSION_V24 && rubricVersion !== WITAN_RUBRIC_VERSION_V23 && findings.length > 0) {
1516
+ if (rubricVersion !== WITAN_RUBRIC_VERSION_V8 && rubricVersion !== WITAN_RUBRIC_VERSION_V9 && rubricVersion !== WITAN_RUBRIC_VERSION_V10 && rubricVersion !== WITAN_RUBRIC_VERSION_V11 && rubricVersion !== WITAN_RUBRIC_VERSION_V12 && rubricVersion !== WITAN_RUBRIC_VERSION_V13 && rubricVersion !== WITAN_RUBRIC_VERSION_V14 && rubricVersion !== WITAN_RUBRIC_VERSION_V15 && rubricVersion !== WITAN_RUBRIC_VERSION_V16 && rubricVersion !== WITAN_RUBRIC_VERSION_V17 && rubricVersion !== WITAN_RUBRIC_VERSION_V27 && rubricVersion !== WITAN_RUBRIC_VERSION_V18 && rubricVersion !== WITAN_RUBRIC_VERSION_V19 && rubricVersion !== WITAN_RUBRIC_VERSION_V20 && rubricVersion !== WITAN_RUBRIC_VERSION_V21 && rubricVersion !== WITAN_RUBRIC_VERSION_V22 && rubricVersion !== WITAN_RUBRIC_VERSION_V25 && rubricVersion !== WITAN_RUBRIC_VERSION_V26 && rubricVersion !== WITAN_RUBRIC_VERSION_V24 && rubricVersion !== WITAN_RUBRIC_VERSION_V23 && findings.length > 0) {
1351
1517
  return [...findings];
1352
1518
  }
1353
1519
  if (status !== "critical" && status !== "warning") return [...findings];
@@ -1857,6 +2023,12 @@ function buildNextSteps(report, unmeasured, notApplicable, a1, coverageMetric) {
1857
2023
  `Resolve the ${actionableSkips.count} content entr${actionableSkips.count === 1 ? "y" : "ies"} skipped as ${joinWithOr(actionableSkips.reasons)} (see the itemized reasons in the certificate), then reproduce the scan.`
1858
2024
  );
1859
2025
  }
2026
+ const lfsPointers = report.contentReadSummary?.byReason.lfsPointer ?? 0;
2027
+ if (lfsPointers > 0) {
2028
+ steps.push(
2029
+ `Fetch the Git LFS content for the ${lfsPointers} file${lfsPointers === 1 ? "" : "s"} whose checkout holds a pointer (git lfs pull), then reproduce the scan.`
2030
+ );
2031
+ }
1860
2032
  if (steps.length === 0) {
1861
2033
  steps.push(
1862
2034
  "No unresolved evidence gaps were identified across the categories Cejel tracks; reproduce the scan at the reported revision when one is available before relying on this certificate."
@@ -1943,7 +2115,9 @@ var WITHHELD_PATH_REASON_LABELS = {
1943
2115
  non_regular_file: "not a regular file",
1944
2116
  unreadable: "unreadable",
1945
2117
  denied_path: "denied path",
1946
- excluded_by_extension: "excluded by extension"
2118
+ excluded_by_extension: "excluded by extension",
2119
+ // Never a withheld-path reason today (an LFS pointer is read, not withheld); the map is total.
2120
+ lfs_pointer: "a Git LFS pointer"
1947
2121
  };
1948
2122
  function describeWithheldReasons(entries) {
1949
2123
  return [...new Set(entries.map((entry) => WITHHELD_PATH_REASON_LABELS[entry.reason]))].sort().join(", ");
@@ -1984,6 +2158,18 @@ function settlingEvidenceDisclosures(report) {
1984
2158
  )
1985
2159
  ).map(({ sentence }) => sentence);
1986
2160
  }
2161
+ function placeholderSetAsideDisclosures(report) {
2162
+ return criterionDisclosures(report, "A2", WITAN_A2_PLACEHOLDER_SET_ASIDE_PREFIX);
2163
+ }
2164
+ function unshownCheckWorkflowDisclosures(report) {
2165
+ const b3 = report.criteria.find((criterion) => criterion.id === "B3");
2166
+ if (!b3 || b3.status === "not_applicable" || b3.status === "insufficient_data") return [];
2167
+ return criterionDisclosures(report, "B3", WITAN_B3_UNSHOWN_CHECK_PREFIX);
2168
+ }
2169
+ function criterionDisclosures(report, criterionId, prefix) {
2170
+ const criterion = report.criteria.find((entry) => entry.id === criterionId);
2171
+ return (criterion?.disclosures ?? []).filter((sentence) => sentence.startsWith(prefix));
2172
+ }
1987
2173
  function describeEstablishedDimensions(report, measured, applicable) {
1988
2174
  const total = report.criteria.length;
1989
2175
  const counts = formatMeasuredDimensionCounts(computeMeasuredCoverage(report));
@@ -2036,6 +2222,12 @@ function buildRelyingPartySummary(report) {
2036
2222
  `${contentEntries(actionableSkips, "was", "were")} skipped for the reasons itemized in the certificate.`
2037
2223
  );
2038
2224
  }
2225
+ const lfsPointerCount = report.contentReadSummary?.byReason.lfsPointer ?? 0;
2226
+ if (lfsPointerCount > 0) {
2227
+ gaps.push(
2228
+ `${lfsPointerCount} file${lfsPointerCount === 1 ? " is a Git LFS pointer" : "s are Git LFS pointers"} in this checkout, so ${lfsPointerCount === 1 ? "its" : "their"} content was not read. A criterion that read one either abstained (it is listed among the affected criteria) or scored the pointer's text.`
2229
+ );
2230
+ }
2039
2231
  const excludedByExtension = report.contentReadSummary?.byReason.excludedByExtension ?? 0;
2040
2232
  if (excludedByExtension > 0) {
2041
2233
  gaps.push(
@@ -2043,6 +2235,8 @@ function buildRelyingPartySummary(report) {
2043
2235
  );
2044
2236
  }
2045
2237
  gaps.push(...settlingEvidenceDisclosures(report));
2238
+ gaps.push(...placeholderSetAsideDisclosures(report));
2239
+ gaps.push(...unshownCheckWorkflowDisclosures(report));
2046
2240
  const withheldPathsSentence = buildWithheldPathsGapSentence(report);
2047
2241
  if (withheldPathsSentence !== void 0) {
2048
2242
  gaps.push(withheldPathsSentence);
@@ -2186,7 +2380,7 @@ function renderWitanHtmlReport(report, options = {}) {
2186
2380
  <aside class="score-panel" aria-label="Summary trust score">
2187
2381
  <div class="score-badge">Summary</div>
2188
2382
  ${report.verdict === "insufficient_source" ? `<div class="verdict">${escapeHtml(renderWitanAbstentionLabel(report))}</div>
2189
- <p class="insufficient-note">${escapeHtml(report.insufficientSourceReason)}</p>` : `<div class="score">${formatScore(report.overallScore)}</div>
2383
+ <p class="insufficient-note">${escapeHtml(report.insufficientSourceReason)}</p>` : `<div class="score">${formatScore2(report.overallScore)}</div>
2190
2384
  <div class="score-unit">/ 4.0 overall</div>
2191
2385
  <div class="verdict">${escapeHtml(renderVerdict(report.overallScore))}</div>
2192
2386
  <div class="subscores">
@@ -2243,6 +2437,8 @@ function renderWitanHtmlReport(report, options = {}) {
2243
2437
  </article>
2244
2438
  </section>` : ""}
2245
2439
 
2440
+ ${renderInputBindingSection(report)}
2441
+
2246
2442
  ${renderGlossary(glossaryEntriesForReport(report))}
2247
2443
  </main>
2248
2444
  </body>
@@ -2307,7 +2503,8 @@ function renderContentReadSummary(report) {
2307
2503
  <li>Too large: ${summary.byReason.tooLarge}</li>
2308
2504
  <li>Excluded by extension: ${summary.byReason.excludedByExtension}</li>
2309
2505
  <li>Denied path: ${summary.byReason.deniedPath}</li>
2310
- <li>Non-regular file: ${summary.byReason.nonRegularFile}</li>
2506
+ <li>Non-regular file: ${summary.byReason.nonRegularFile}</li>${summary.byReason.lfsPointer ? `
2507
+ <li>Git LFS pointer (content not in this checkout): ${summary.byReason.lfsPointer}</li>` : ""}
2311
2508
  </ul>
2312
2509
  ${affected}
2313
2510
  </section>`;
@@ -2342,7 +2539,7 @@ function formatSourceProvenanceLine(summary) {
2342
2539
  function renderExternalFindingItems(findings) {
2343
2540
  const shown = findings.slice(0, EXTERNAL_FINDINGS_DISPLAY_LIMIT);
2344
2541
  const items = shown.map(
2345
- (f) => `<li><strong>${escapeHtml(f.label)} \u2192 ${escapeHtml(f.dimension)}</strong><span>${escapeHtml(f.severity)}: ${escapeHtml(f.ruleId)} \u2014 ${escapeHtml(f.message)}${f.location ? ` (${escapeHtml(f.location)})` : ""}</span></li>`
2542
+ (f) => `<li><strong>${escapeHtml(f.label)} \u2192 ${escapeHtml(f.dimension)}</strong><span>${escapeHtml(f.severity)}: ${escapeHtml(f.ruleId)} \u2014 ${escapeHtml(f.message)}${f.location ? ` (${escapeHtml(f.location)})` : ""}</span>${renderInheritedFindingNote(f.ruleId)}</li>`
2346
2543
  );
2347
2544
  const remaining = findings.length - shown.length;
2348
2545
  if (remaining > 0) {
@@ -2350,6 +2547,21 @@ function renderExternalFindingItems(findings) {
2350
2547
  }
2351
2548
  return items;
2352
2549
  }
2550
+ function renderInheritedFindingNote(ruleId) {
2551
+ const note = inheritedFindingNote(ruleId);
2552
+ return note ? `<span class="external-note">${escapeHtml(note)}</span>` : "";
2553
+ }
2554
+ function renderInputBindingSection(report) {
2555
+ const binding = report.inputBinding;
2556
+ if (!binding) return "";
2557
+ return `<section class="external-findings-section" aria-label="Ingested inputs">
2558
+ <article class="evidence-section">
2559
+ <h2>Ingested inputs</h2>
2560
+ <p class="external-note">${escapeHtml(inputBindingHeadline(binding))} ${escapeHtml(describeScannedSubject(binding))}</p>
2561
+ <ul>${binding.inputs.map((input) => `<li>${escapeHtml(describeInputBindingRecord(input))}</li>`).join("")}</ul>
2562
+ </article>
2563
+ </section>`;
2564
+ }
2353
2565
  function criteriaByCategory(report, category) {
2354
2566
  return report.criteria.filter((criterion) => criterion.category === category);
2355
2567
  }
@@ -2393,8 +2605,8 @@ function renderCriterionCard(criterion, gitHistoryUnavailable) {
2393
2605
  </div>
2394
2606
  ${renderStatusChip(criterion.id, criterion.status)}
2395
2607
  </div>
2396
- <div class="criterion-score">${criterion.status === "not_applicable" ? "N/A" : criterion.status === "insufficient_data" ? "No data" : formatScore(criterion.score)}</div>
2397
- ${statusReconciliation}
2608
+ <div class="criterion-score">${criterion.status === "not_applicable" ? "N/A" : criterion.status === "insufficient_data" ? "No data" : formatScore2(criterion.score)}</div>
2609
+ ${statusReconciliation}${renderImportedAdjustment(criterion)}
2398
2610
  ${historyWarning}
2399
2611
  <ul class="criterion-metrics">
2400
2612
  ${metrics.length > 0 ? metrics.map((item) => `<li>${item}</li>`).join("") : "<li>No measured depth metrics supplied.</li>"}
@@ -2404,6 +2616,11 @@ function renderCriterionCard(criterion, gitHistoryUnavailable) {
2404
2616
  </ul>
2405
2617
  </article>`;
2406
2618
  }
2619
+ function renderImportedAdjustment(criterion) {
2620
+ const sentence = importedAdjustmentSentence(criterion);
2621
+ return sentence ? `
2622
+ <p class="status-explanation">${escapeHtml(sentence)}</p>` : "";
2623
+ }
2407
2624
  function renderStatusReconciliation(criterion) {
2408
2625
  if (criterion.status === "not_applicable") {
2409
2626
  return '<p class="status-explanation"><strong>Why no label comparison applies:</strong> this dimension is not applicable to this repository, so it has no weighted numeric score band to compare.</p>';
@@ -2416,14 +2633,14 @@ function renderStatusReconciliation(criterion) {
2416
2633
  }
2417
2634
  const numericBand = scoreBandForPresentation(criterion.score);
2418
2635
  if (numericBand === criterion.status) {
2419
- return `<p class="status-explanation"><strong>How the labels agree:</strong> the ${escapeHtml(criterion.status)} dimension band matches the ${formatScore(criterion.score)}/4.0 weighted score (the ${numericBand} numeric band).</p>`;
2636
+ return `<p class="status-explanation"><strong>How the labels agree:</strong> the ${escapeHtml(criterion.status)} dimension band matches the ${formatScore2(criterion.score)}/4.0 weighted score (the ${numericBand} numeric band).</p>`;
2420
2637
  }
2421
2638
  if (criterion.status !== "verified" && criterion.status !== "info" && criterion.status !== "warning" && criterion.status !== "critical") {
2422
2639
  return `<p class="status-explanation"><strong>Why no label comparison applies:</strong> this report contains an unrecognized dimension state (${escapeHtml(String(criterion.status))}), so Cejel will not infer a comparison.</p>`;
2423
2640
  }
2424
2641
  const entry = CERTIFICATE_GLOSSARY.find((candidate) => candidate.key === "labels");
2425
2642
  const help = entry ? `<span class="term-help" tabindex="0" aria-describedby="tooltip-${criterion.id}-labels"><strong>Why the labels differ:</strong><span class="term-tooltip" id="tooltip-${criterion.id}-labels" role="tooltip">${escapeHtml(entry.definition)}</span></span>` : "<strong>Why the labels differ:</strong>";
2426
- return `<p class="status-explanation">${help} 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>`;
2643
+ return `<p class="status-explanation">${help} the ${escapeHtml(criterion.status)} dimension band is calibrated from criterion evidence thresholds and findings, independently of the ${formatScore2(criterion.score)}/4.0 weighted score (which falls in the ${numericBand} numeric band).</p>`;
2427
2644
  }
2428
2645
  function scoreBandForPresentation(score) {
2429
2646
  if (score >= 3.5) return "verified";
@@ -2449,7 +2666,7 @@ function renderMetric(criterion, metric2) {
2449
2666
  }
2450
2667
  function renderCategoryScore(score, coverage, category) {
2451
2668
  const categoryCoverage = coverage.byCategory.find((entry) => entry.category === category);
2452
- return categoryCoverage !== void 0 && categoryCoverage.measured === 0 || score === null ? "not measured" : formatScore(score);
2669
+ return categoryCoverage !== void 0 && categoryCoverage.measured === 0 || score === null ? "not measured" : formatScore2(score);
2453
2670
  }
2454
2671
  function renderStatusChip(criterionId, status) {
2455
2672
  const entry = CERTIFICATE_GLOSSARY.find((candidate) => candidate.key === "labels");
@@ -2515,7 +2732,7 @@ function renderMachineVerdict(verdict) {
2515
2732
  return "Insufficient source";
2516
2733
  }
2517
2734
  }
2518
- function formatScore(score) {
2735
+ function formatScore2(score) {
2519
2736
  return score.toFixed(1);
2520
2737
  }
2521
2738
  function formatDate(value) {
@@ -2706,7 +2923,7 @@ function badgeMessage(report, verdict) {
2706
2923
  if ((report.scanLimitations?.length ?? 0) > 0) return "unrated: limited evidence";
2707
2924
  if (isWitanNoMeasurementAbstention(report)) return "unrated: no measurable evidence";
2708
2925
  if (report.verdict === "insufficient_source") return "unrated: no source";
2709
- return `${formatScore2(report.overallScore)}/4.0 ${verdict.toLowerCase()}`;
2926
+ return `${formatScore3(report.overallScore)}/4.0 ${verdict.toLowerCase()}`;
2710
2927
  }
2711
2928
  function renderWitanBadgeEndpoint(report) {
2712
2929
  const verdict = renderReportVerdict(report);
@@ -2750,7 +2967,7 @@ function renderWitanBadgeSvg(report) {
2750
2967
  </svg>
2751
2968
  `;
2752
2969
  }
2753
- function formatScore2(score) {
2970
+ function formatScore3(score) {
2754
2971
  return score.toFixed(1);
2755
2972
  }
2756
2973
  function escapeXml(value) {
@@ -2887,12 +3104,6 @@ function verifyWitanAttestationBinding(statement, report, options = {}) {
2887
3104
  return { valid: errors.length === 0, errors };
2888
3105
  }
2889
3106
 
2890
- // src/witan/index.ts
2891
- init_esm_shims();
2892
-
2893
- // src/witan/markdown.ts
2894
- init_esm_shims();
2895
-
2896
3107
  // src/presentation-safety.ts
2897
3108
  init_esm_shims();
2898
3109
  var PRESENTATION_CONTROL_PATTERN = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu;
@@ -2909,17 +3120,530 @@ function sanitizePresentationLine(value, options) {
2909
3120
  }
2910
3121
  return `${prefix}${ellipsis}`;
2911
3122
  }
3123
+ function stripTerminalControls(text) {
3124
+ return text.replace(/[\t\n]/g, " ").replace(/[\u0000-\u001f\u007f-\u009f]/g, "");
3125
+ }
2912
3126
 
2913
- // src/witan/repo-signals.ts
3127
+ // src/witan/input-binding.ts
2914
3128
  init_esm_shims();
2915
3129
  import { createHash as createHash3 } from "crypto";
3130
+
3131
+ // src/witan/linear-strips.ts
3132
+ init_esm_shims();
3133
+ var LINE_TERMINATOR_PATTERN = /[\n\r\u2028\u2029]/;
3134
+ var WHITESPACE_PATTERN = /\s/;
3135
+ function lastLineTerminator(text) {
3136
+ for (let index = text.length - 1; index >= 0; index -= 1) {
3137
+ if (LINE_TERMINATOR_PATTERN.test(text[index] ?? "")) return index;
3138
+ }
3139
+ return -1;
3140
+ }
3141
+ function stripTrailingHashComment(text) {
3142
+ for (let index = text.indexOf("#", lastLineTerminator(text) + 1); index !== -1; index = text.indexOf("#", index + 1)) {
3143
+ if (index === 0 || !WHITESPACE_PATTERN.test(text[index - 1] ?? "")) continue;
3144
+ return text.slice(0, index).trimEnd();
3145
+ }
3146
+ return text;
3147
+ }
3148
+ function stripHashCommentAtStartOrAfterBlank(text) {
3149
+ for (let index = text.indexOf("#", lastLineTerminator(text) + 1); index !== -1; index = text.indexOf("#", index + 1)) {
3150
+ if (index === 0) return "";
3151
+ if (WHITESPACE_PATTERN.test(text[index - 1] ?? "")) return text.slice(0, index - 1);
3152
+ }
3153
+ return text;
3154
+ }
3155
+ function stripFromQueryOrFragment(text) {
3156
+ const from = lastLineTerminator(text) + 1;
3157
+ for (let index = from; index < text.length; index += 1) {
3158
+ const character = text[index];
3159
+ if (character === "?" || character === "#") return text.slice(0, index);
3160
+ }
3161
+ return text;
3162
+ }
3163
+ function stripTrailingCloseParentheses(segment) {
3164
+ let end = segment.length;
3165
+ while (end > 0 && segment[end - 1] === ")") end -= 1;
3166
+ if (end === segment.length) return segment;
3167
+ return segment.slice(0, end).trimEnd();
3168
+ }
3169
+ function stripTrailingSlashes(text) {
3170
+ let end = text.length;
3171
+ while (end > 0 && text[end - 1] === "/") end -= 1;
3172
+ return end === text.length ? text : text.slice(0, end);
3173
+ }
3174
+ function hasScheduleCronTrigger(text) {
3175
+ const key = "schedule:";
3176
+ for (let index = text.indexOf(key); index !== -1; index = text.indexOf(key, index + 1)) {
3177
+ let before = index - 1;
3178
+ let newlineBefore = false;
3179
+ while (before >= 0 && WHITESPACE_PATTERN.test(text[before] ?? "")) {
3180
+ if (text[before] === "\n") newlineBefore = true;
3181
+ before -= 1;
3182
+ }
3183
+ if (before !== -1 && !newlineBefore) continue;
3184
+ let after = index + key.length;
3185
+ let newlineAfter = false;
3186
+ while (after < text.length && WHITESPACE_PATTERN.test(text[after] ?? "")) {
3187
+ if (text[after] === "\n") newlineAfter = true;
3188
+ after += 1;
3189
+ }
3190
+ if (!newlineAfter || text[after] !== "-") continue;
3191
+ after += 1;
3192
+ while (after < text.length && WHITESPACE_PATTERN.test(text[after] ?? "")) after += 1;
3193
+ if (text.startsWith("cron:", after)) return true;
3194
+ }
3195
+ return false;
3196
+ }
3197
+ function isShellScriptPath(text) {
3198
+ if (!/\.sh$/i.test(text.slice(-3))) return false;
3199
+ let start = text.length;
3200
+ while (start > 0 && !WHITESPACE_PATTERN.test(text[start - 1] ?? "")) start -= 1;
3201
+ if (start === 0) return true;
3202
+ const slash = text.indexOf("/", start);
3203
+ return slash !== -1 && text.length - (slash + 1) >= 3;
3204
+ }
3205
+ function execOnTerminatorFreeLine(pattern, line) {
3206
+ return LINE_TERMINATOR_PATTERN.test(line) ? null : pattern.exec(line);
3207
+ }
3208
+ var VERSION_OPERATOR_CHARACTERS = /* @__PURE__ */ new Set([">", "<", "=", "~", "!", "^"]);
3209
+ var VERSION_DIGIT_PATTERN = /[\d.]/;
3210
+ function firstVersionConstraint(text) {
3211
+ let index = 0;
3212
+ while (index < text.length) {
3213
+ if (!VERSION_OPERATOR_CHARACTERS.has(text[index] ?? "")) {
3214
+ index += 1;
3215
+ continue;
3216
+ }
3217
+ const start = index;
3218
+ while (index < text.length && VERSION_OPERATOR_CHARACTERS.has(text[index] ?? "")) index += 1;
3219
+ if (index < text.length && VERSION_DIGIT_PATTERN.test(text[index] ?? "")) {
3220
+ let end = index;
3221
+ while (end < text.length && VERSION_DIGIT_PATTERN.test(text[end] ?? "")) end += 1;
3222
+ return text.slice(start, end);
3223
+ }
3224
+ }
3225
+ return void 0;
3226
+ }
3227
+
3228
+ // src/witan/input-binding.ts
3229
+ var INGEST_FRESHNESS_WINDOW_SECONDS = 86400;
3230
+ var MAX_IDENTITIES_PER_INPUT = 8;
3231
+ var MAX_PRODUCER_NAME_LENGTH = 120;
3232
+ var MAX_PRODUCER_VERSION_LENGTH = 80;
3233
+ var MAX_REPOSITORY_LENGTH = 300;
3234
+ var MAX_COMMIT_LENGTH = 80;
3235
+ var COMMIT_PATTERN = /^[0-9a-f]{7,64}$/;
3236
+ var FULL_COMMIT_LENGTHS = /* @__PURE__ */ new Set([40, 64]);
3237
+ function sha256OfBytes(bytes) {
3238
+ return createHash3("sha256").update(bytes).digest("hex");
3239
+ }
3240
+ function normalizeRepositoryIdentity(value) {
3241
+ let text = value.trim();
3242
+ if (text.length === 0 || text.length > 2048) return void 0;
3243
+ const scp = /^[^@/\s]+@([^:/\s]+):(.+)$/.exec(text);
3244
+ if (scp) text = `${scp[1]}/${scp[2]}`;
3245
+ else {
3246
+ const scheme = /^[a-z][a-z0-9+.-]*:\/\/(.*)$/i.exec(text);
3247
+ if (scheme) text = scheme[1] ?? "";
3248
+ const at = text.indexOf("@");
3249
+ const slash = text.indexOf("/");
3250
+ if (at !== -1 && (slash === -1 || at < slash)) text = text.slice(at + 1);
3251
+ }
3252
+ text = stripFromQueryOrFragment(text);
3253
+ const [hostWithPort, ...pathParts] = text.split("/");
3254
+ const host = (hostWithPort ?? "").replace(/:\d+$/, "").toLowerCase();
3255
+ const path = pathParts.filter((part) => part.length > 0).join("/").replace(/(?:\/|\.git)+$/i, "").toLowerCase();
3256
+ if (!/^[a-z0-9.-]+\.[a-z]{2,}$|^localhost$/.test(host) || path.length === 0) return void 0;
3257
+ if (/[\p{Cc}\p{Cf}\s]/u.test(path)) return void 0;
3258
+ if (path.includes("@")) return void 0;
3259
+ const normalized = `${host}/${path}`;
3260
+ return normalized.length <= MAX_REPOSITORY_LENGTH ? normalized : void 0;
3261
+ }
3262
+ function normalizeCommit(value) {
3263
+ if (typeof value !== "string") return void 0;
3264
+ const trimmed = value.trim().toLowerCase();
3265
+ if (trimmed.length === 0) return void 0;
3266
+ return sanitizePresentationLine(trimmed, { fallback: "unknown", maxLength: MAX_COMMIT_LENGTH });
3267
+ }
3268
+ function boundedText(value, maxLength) {
3269
+ if (typeof value !== "string" || value.trim().length === 0) return void 0;
3270
+ return sanitizePresentationLine(value, { fallback: "unknown", maxLength });
3271
+ }
3272
+ function parseObservedAt(value) {
3273
+ if (typeof value !== "string") return void 0;
3274
+ const trimmed = value.trim();
3275
+ const dateOnly = /^(\d{4})-(\d{2})-(\d{2})$/.exec(trimmed);
3276
+ if (dateOnly) return plainIsoInstant(Date.parse(`${trimmed}T23:59:59Z`));
3277
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})$/i.test(trimmed)) {
3278
+ return void 0;
3279
+ }
3280
+ return plainIsoInstant(Date.parse(trimmed));
3281
+ }
3282
+ var PLAIN_ISO_INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
3283
+ function plainIsoInstant(millis) {
3284
+ if (!Number.isFinite(millis)) return void 0;
3285
+ const date = new Date(millis);
3286
+ if (Number.isNaN(date.getTime())) return void 0;
3287
+ const iso = date.toISOString();
3288
+ return PLAIN_ISO_INSTANT.test(iso) ? iso : void 0;
3289
+ }
3290
+ function earliest(values) {
3291
+ let best;
3292
+ let bestMillis = Number.POSITIVE_INFINITY;
3293
+ for (const value of values) {
3294
+ if (value === void 0) continue;
3295
+ const millis = Date.parse(value);
3296
+ if (Number.isNaN(millis)) continue;
3297
+ if (millis < bestMillis) {
3298
+ best = value;
3299
+ bestMillis = millis;
3300
+ }
3301
+ }
3302
+ return best;
3303
+ }
3304
+ var SubjectSet = class {
3305
+ byKey = /* @__PURE__ */ new Map();
3306
+ add(subject) {
3307
+ const key = JSON.stringify([subject.repository ?? null, subject.repositoryWithheld ?? false, subject.commit ?? null]);
3308
+ if (!this.byKey.has(key)) this.byKey.set(key, subject);
3309
+ }
3310
+ get size() {
3311
+ return this.byKey.size;
3312
+ }
3313
+ values() {
3314
+ return [...this.byKey.values()];
3315
+ }
3316
+ };
3317
+ function pushUnique(items, item) {
3318
+ if (items.length >= MAX_IDENTITIES_PER_INPUT) return;
3319
+ const key = JSON.stringify(item);
3320
+ if (!items.some((existing) => JSON.stringify(existing) === key)) items.push(item);
3321
+ }
3322
+ function subjectFrom(repository, commit) {
3323
+ const declaredRepository = repository !== void 0 && repository !== null && !(typeof repository === "string" && repository.trim().length === 0);
3324
+ const normalizedRepository = typeof repository === "string" && declaredRepository ? normalizeRepositoryIdentity(repository) : void 0;
3325
+ const normalizedCommit = normalizeCommit(commit);
3326
+ if (!declaredRepository && normalizedCommit === void 0) return void 0;
3327
+ return {
3328
+ ...normalizedRepository !== void 0 ? { repository: normalizedRepository } : declaredRepository ? { repositoryWithheld: true } : {},
3329
+ ...normalizedCommit !== void 0 ? { commit: normalizedCommit } : {}
3330
+ };
3331
+ }
3332
+ function asRecord(value) {
3333
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
3334
+ }
3335
+ function extractScorecardIdentity(raw) {
3336
+ const doc = asRecord(raw) ?? {};
3337
+ const repo = asRecord(doc.repo) ?? {};
3338
+ const scorecard = asRecord(doc.scorecard) ?? {};
3339
+ const subject = subjectFrom(repo.name, repo.commit);
3340
+ const version = boundedText(scorecard.version, MAX_PRODUCER_VERSION_LENGTH);
3341
+ const commit = normalizeCommit(scorecard.commit);
3342
+ const observedAt = parseObservedAt(doc.date);
3343
+ return {
3344
+ producers: [
3345
+ {
3346
+ name: "scorecard",
3347
+ ...version !== void 0 ? { version } : {},
3348
+ ...commit !== void 0 ? { commit } : {}
3349
+ }
3350
+ ],
3351
+ subjects: subject ? [subject] : [],
3352
+ ...observedAt !== void 0 ? { observedAt } : {}
3353
+ };
3354
+ }
3355
+ function extractSarifIdentity(raw) {
3356
+ const doc = asRecord(raw) ?? {};
3357
+ const runs = Array.isArray(doc.runs) ? doc.runs : [];
3358
+ const producers = [];
3359
+ const subjects = new SubjectSet();
3360
+ const times = [];
3361
+ let runWithoutSubject = false;
3362
+ for (const rawRun of runs) {
3363
+ const run = asRecord(rawRun) ?? {};
3364
+ const driver = asRecord(asRecord(run.tool)?.driver) ?? {};
3365
+ const name = boundedText(driver.name, MAX_PRODUCER_NAME_LENGTH) ?? "unknown";
3366
+ const version = boundedText(driver.semanticVersion ?? driver.version, MAX_PRODUCER_VERSION_LENGTH);
3367
+ pushUnique(producers, { name, ...version !== void 0 ? { version } : {} });
3368
+ const provenance = Array.isArray(run.versionControlProvenance) ? run.versionControlProvenance : [];
3369
+ let runDeclaredSubject = false;
3370
+ for (const rawEntry of provenance) {
3371
+ const entry = asRecord(rawEntry) ?? {};
3372
+ const subject = subjectFrom(entry.repositoryUri, entry.revisionId);
3373
+ if (subject) {
3374
+ subjects.add(subject);
3375
+ runDeclaredSubject = true;
3376
+ }
3377
+ }
3378
+ if (!runDeclaredSubject) runWithoutSubject = true;
3379
+ const invocations = Array.isArray(run.invocations) ? run.invocations : [];
3380
+ for (const rawInvocation of invocations) {
3381
+ const invocation = asRecord(rawInvocation) ?? {};
3382
+ times.push(parseObservedAt(invocation.startTimeUtc), parseObservedAt(invocation.endTimeUtc));
3383
+ }
3384
+ }
3385
+ if (runWithoutSubject && subjects.size > 0) subjects.add({});
3386
+ const observedAt = earliest(times);
3387
+ return {
3388
+ producers,
3389
+ subjects: subjects.values(),
3390
+ ...observedAt !== void 0 ? { observedAt } : {}
3391
+ };
3392
+ }
3393
+ function genericIdentity(fields) {
3394
+ const name = boundedText(fields.producer?.name ?? fields.tool, MAX_PRODUCER_NAME_LENGTH) ?? "unknown";
3395
+ const version = boundedText(fields.producer?.version, MAX_PRODUCER_VERSION_LENGTH);
3396
+ const subject = fields.subject ? subjectFrom(fields.subject.repository, fields.subject.commit) : void 0;
3397
+ const observedAt = parseObservedAt(fields.observedAt);
3398
+ return {
3399
+ producers: [{ name, ...version !== void 0 ? { version } : {} }],
3400
+ subjects: subject ? [subject] : [],
3401
+ ...observedAt !== void 0 ? { observedAt } : {}
3402
+ };
3403
+ }
3404
+ function declaresRepository(subject) {
3405
+ return subject.repository !== void 0 || subject.repositoryWithheld === true;
3406
+ }
3407
+ function checkRepository(subjects, scanned) {
3408
+ const declared = subjects.filter(declaresRepository);
3409
+ if (declared.length === 0) return "absent";
3410
+ if (scanned.repository === void 0) return "unverifiable";
3411
+ return declared.every((subject) => subject.repository === scanned.repository) ? "matched" : "mismatched";
3412
+ }
3413
+ function subjectDisagrees(subject, scanned) {
3414
+ if (scanned.repository !== void 0 && declaresRepository(subject)) {
3415
+ if (subject.repository !== scanned.repository) return true;
3416
+ }
3417
+ if (scanned.commit !== void 0 && subject.commit !== void 0) {
3418
+ if (!commitMatches(subject.commit, scanned.commit)) return true;
3419
+ }
3420
+ return false;
3421
+ }
3422
+ function commitMatches(inputCommit, headSha) {
3423
+ if (!COMMIT_PATTERN.test(inputCommit)) return false;
3424
+ const head = headSha.toLowerCase();
3425
+ if (FULL_COMMIT_LENGTHS.has(inputCommit.length)) return inputCommit === head;
3426
+ return head.startsWith(inputCommit);
3427
+ }
3428
+ function checkCommit(subjects, scanned) {
3429
+ const commits = subjects.map((subject) => subject.commit).filter((commit) => commit !== void 0);
3430
+ if (commits.length === 0) return "absent";
3431
+ if (scanned.commit === void 0) return "unverifiable";
3432
+ const head = scanned.commit;
3433
+ return commits.every((commit) => commitMatches(commit, head)) ? "matched" : "mismatched";
3434
+ }
3435
+ function checkFreshness(observedAt, scanned, windowSeconds) {
3436
+ if (observedAt === void 0) return "absent";
3437
+ if (scanned.commitTime === void 0) return "unverifiable";
3438
+ const observed = Date.parse(observedAt);
3439
+ const committed = Date.parse(scanned.commitTime);
3440
+ return observed < committed - windowSeconds * 1e3 ? "stale" : "within-window";
3441
+ }
3442
+ function bindingStatusForChecks(checks, everySubjectDeclaresCommit = true) {
3443
+ if (checks.repository === "mismatched") return "mismatched-repository";
3444
+ if (checks.commit === "mismatched") return "mismatched-commit";
3445
+ if (checks.freshness === "stale") return "stale";
3446
+ if (checks.commit === "matched" && !everySubjectDeclaresCommit) return "unbound";
3447
+ if (checks.commit === "matched") return "matched";
3448
+ if (checks.commit === "unverifiable") return "unverifiable";
3449
+ return "unbound";
3450
+ }
3451
+ function recordedSubjects(subjects, scanned) {
3452
+ if (subjects.length <= MAX_IDENTITIES_PER_INPUT) return { subjects: [...subjects], omitted: 0 };
3453
+ const rank = (subject) => subjectDisagrees(subject, scanned) ? 0 : subject.commit === void 0 ? 1 : 2;
3454
+ const ordered = subjects.map((subject, index) => ({ subject, index, rank: rank(subject) })).sort((a, b) => a.rank - b.rank || a.index - b.index).slice(0, MAX_IDENTITIES_PER_INPUT).map((entry) => entry.subject);
3455
+ return { subjects: ordered, omitted: subjects.length - MAX_IDENTITIES_PER_INPUT };
3456
+ }
3457
+ function evaluateInputBindingRecord(document, scanned, windowSeconds = INGEST_FRESHNESS_WINDOW_SECONDS) {
3458
+ const allSubjects = document.identity.subjects;
3459
+ const checks = {
3460
+ repository: checkRepository(allSubjects, scanned),
3461
+ commit: checkCommit(allSubjects, scanned),
3462
+ freshness: checkFreshness(document.identity.observedAt, scanned, windowSeconds)
3463
+ };
3464
+ const recorded = recordedSubjects(allSubjects, scanned);
3465
+ return {
3466
+ sha256: document.sha256,
3467
+ format: document.format,
3468
+ provenance: document.provenance,
3469
+ producers: [...document.identity.producers],
3470
+ subjects: recorded.subjects,
3471
+ ...recorded.omitted > 0 ? { subjectsOmitted: recorded.omitted } : {},
3472
+ ...document.identity.observedAt !== void 0 ? { observedAt: document.identity.observedAt } : {},
3473
+ status: bindingStatusForChecks(
3474
+ checks,
3475
+ allSubjects.every((subject) => subject.commit !== void 0)
3476
+ ),
3477
+ checks,
3478
+ ...document.duplicate !== void 0 ? { duplicate: document.duplicate } : {},
3479
+ scored: document.duplicate !== "same-path"
3480
+ };
3481
+ }
3482
+ function compareRecords(a, b) {
3483
+ return a.sha256.localeCompare(b.sha256) || (a.duplicate ?? "").localeCompare(b.duplicate ?? "") || a.provenance.localeCompare(b.provenance);
3484
+ }
3485
+ function buildInputBinding(documents, scanned, policy, windowSeconds = INGEST_FRESHNESS_WINDOW_SECONDS) {
3486
+ return {
3487
+ policy,
3488
+ freshnessWindowSeconds: windowSeconds,
3489
+ scannedSubject: {
3490
+ ...scanned.repository !== void 0 ? { repository: scanned.repository } : {},
3491
+ ...scanned.commit !== void 0 ? { commit: scanned.commit } : {},
3492
+ ...scanned.commitTime !== void 0 ? { commitTime: scanned.commitTime } : {}
3493
+ },
3494
+ inputs: documents.map((document) => evaluateInputBindingRecord(document, scanned, windowSeconds)).sort(compareRecords)
3495
+ };
3496
+ }
3497
+ var STRICT_INGEST_REFUSED_STATUSES = /* @__PURE__ */ new Set([
3498
+ "mismatched-repository",
3499
+ "mismatched-commit",
3500
+ "stale"
3501
+ ]);
3502
+ function strictIngestRefusalReasons(record) {
3503
+ const reasons = [];
3504
+ if (STRICT_INGEST_REFUSED_STATUSES.has(record.status)) reasons.push(record.status);
3505
+ if (record.duplicate !== void 0) reasons.push(`duplicate (${record.duplicate})`);
3506
+ return reasons;
3507
+ }
3508
+ function describeSubjects(record) {
3509
+ if (record.subjects.length === 0) return "no subject";
3510
+ const listed = record.subjects.map(
3511
+ (subject) => `${subject.repository ?? (subject.repositoryWithheld ? "(repository withheld: not normalizable)" : "?")}@${subject.commit ?? "?"}`
3512
+ ).join(", ");
3513
+ return record.subjectsOmitted ? `${listed} and ${record.subjectsOmitted} more` : listed;
3514
+ }
3515
+ function assertStrictIngestBinding(documents, binding) {
3516
+ const problems = [];
3517
+ for (const document of documents) {
3518
+ const record = evaluateInputBindingRecord(
3519
+ document,
3520
+ binding.scannedSubject,
3521
+ binding.freshnessWindowSeconds
3522
+ );
3523
+ const reasons = strictIngestRefusalReasons(record);
3524
+ if (reasons.length === 0) continue;
3525
+ problems.push(
3526
+ // displayPath is the operator's or repository's own path text; a committed file name can
3527
+ // carry terminal controls, and this message goes to stderr.
3528
+ ` - ${stripTerminalControls(document.displayPath)} (sha256:${record.sha256.slice(0, 12)}, ${record.format}): ${reasons.join(", ")}; input subject ${describeSubjects(record)}${record.observedAt ? `, observed ${record.observedAt}` : ""}`
3529
+ );
3530
+ }
3531
+ if (problems.length === 0) return;
3532
+ const scanned = binding.scannedSubject;
3533
+ throw new Error(
3534
+ [
3535
+ `Cejel: --strict-ingest refused ${problems.length} ingest input${problems.length === 1 ? "" : "s"}; no certificate was written.`,
3536
+ ...problems,
3537
+ ` scanned subject: ${scanned.repository ?? "(repository not named; pass --subject-repository to compare it)"}@${scanned.commit ?? "(no git HEAD)"}${scanned.commitTime ? `, committed ${scanned.commitTime}` : ""}; freshness window ${binding.freshnessWindowSeconds}s before that commit time.`,
3538
+ " Supply inputs produced for this repository at this commit, remove duplicates, or drop --strict-ingest to record the binding status without refusing."
3539
+ ].join("\n")
3540
+ );
3541
+ }
3542
+ function verifyRecordedInputDigests(report, inputPaths, readFile) {
3543
+ const binding = report.inputBinding;
3544
+ if (!binding) {
3545
+ return {
3546
+ valid: false,
3547
+ lines: [
3548
+ "Cejel: input digests not verified \u2014 report.json records no inputBinding (it ingested nothing, or predates report format 1.7)."
3549
+ ]
3550
+ };
3551
+ }
3552
+ const recorded = new Set(binding.inputs.filter((input) => input.scored).map((input) => input.sha256));
3553
+ const supplied = /* @__PURE__ */ new Map();
3554
+ for (const path of inputPaths) supplied.set(sha256OfBytes(readFile(path)), path);
3555
+ const lines = [];
3556
+ for (const [digest, path] of supplied) {
3557
+ if (!recorded.has(digest)) {
3558
+ lines.push(`Cejel: input ${path} (sha256:${digest}) is not an input report.json records.`);
3559
+ }
3560
+ }
3561
+ for (const digest of recorded) {
3562
+ if (!supplied.has(digest)) {
3563
+ lines.push(`Cejel: report.json records input sha256:${digest}, which no supplied --input matches.`);
3564
+ }
3565
+ }
3566
+ if (lines.length > 0) return { valid: false, lines };
3567
+ return {
3568
+ valid: true,
3569
+ lines: [`Cejel: ${recorded.size} ingested input digest${recorded.size === 1 ? "" : "s"} verified against report.json.`]
3570
+ };
3571
+ }
3572
+
3573
+ // src/witan/ingest-files.ts
3574
+ init_esm_shims();
3575
+ import { lstatSync, realpathSync } from "fs";
3576
+ import { dirname, isAbsolute, relative, resolve, sep } from "path";
3577
+ var MAX_INGEST_DOCUMENT_BYTES = 8 * 1024 * 1024;
3578
+ function isContainedPath(candidate, directory) {
3579
+ const relativePath = relative(directory, candidate);
3580
+ return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath);
3581
+ }
3582
+ function isMissingIngestPath(error) {
3583
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
3584
+ }
3585
+ function resolveIngestFilePath(filePath, inputDirectory = dirname(filePath)) {
3586
+ const lexicalDirectory = resolve(inputDirectory);
3587
+ const lexicalCandidate = resolve(filePath);
3588
+ const directoryStat = lstatSync(lexicalDirectory);
3589
+ if (directoryStat.isSymbolicLink()) {
3590
+ throw new Error(`Cejel: refusing symlinked ingest directory: ${inputDirectory}`);
3591
+ }
3592
+ if (!directoryStat.isDirectory()) {
3593
+ throw new Error(`Cejel: refusing non-directory ingest boundary: ${inputDirectory}`);
3594
+ }
3595
+ const candidateStat = lstatSync(lexicalCandidate);
3596
+ if (candidateStat.isSymbolicLink()) {
3597
+ throw new Error(`Cejel: refusing symlinked ingest file: ${filePath}`);
3598
+ }
3599
+ if (!candidateStat.isFile()) {
3600
+ throw new Error(`Cejel: refusing non-regular ingest file: ${filePath}`);
3601
+ }
3602
+ if (candidateStat.size > MAX_INGEST_DOCUMENT_BYTES) {
3603
+ throw new Error(
3604
+ `Cejel: ingest file exceeds the ${MAX_INGEST_DOCUMENT_BYTES.toLocaleString("en-US")} byte budget: ${filePath}`
3605
+ );
3606
+ }
3607
+ const resolvedDirectory = realpathSync(lexicalDirectory);
3608
+ const resolvedCandidate = realpathSync(lexicalCandidate);
3609
+ if (!isContainedPath(resolvedCandidate, resolvedDirectory)) {
3610
+ throw new Error(`Cejel: refusing ingest file outside its resolved input directory: ${filePath}`);
3611
+ }
3612
+ return resolvedCandidate;
3613
+ }
3614
+ function isResolvedIngestPathContained(candidate, directory) {
3615
+ return isContainedPath(candidate, directory);
3616
+ }
3617
+
3618
+ // src/witan/index.ts
3619
+ init_esm_shims();
3620
+
3621
+ // src/witan/markdown.ts
3622
+ init_esm_shims();
3623
+
3624
+ // src/witan/repo-signals.ts
3625
+ init_esm_shims();
3626
+ import { createHash as createHash4 } from "crypto";
2916
3627
  import {
2917
- lstatSync,
3628
+ lstatSync as lstatSync2,
2918
3629
  readdirSync,
2919
- realpathSync,
3630
+ realpathSync as realpathSync2,
2920
3631
  statSync
2921
3632
  } from "fs";
2922
- import { basename, dirname, extname, join, posix, relative, resolve, sep } from "path";
3633
+ import { basename, dirname as dirname2, extname, join, posix, relative as relative2, resolve as resolve2, sep as sep2 } from "path";
3634
+
3635
+ // src/witan/history-depth.ts
3636
+ init_esm_shims();
3637
+ var SHALLOW_HISTORY_LIMITATION = "This repository is a shallow clone (git rev-parse --is-shallow-repository reports true), so its commit history is truncated. A2 (credentials and .env files in earlier commits) and B2 (pull-request merge ratio over recent commits) read history and saw only the commits present; on a full clone their results may differ. Scores are computed exactly as for any other scan. Re-scan a full clone (for actions/checkout, set fetch-depth: 0).";
3638
+ var SHALLOW_HISTORY_V27_ABSTENTION = "This repository is a shallow clone (git rev-parse --is-shallow-repository reports true), so its commit history is truncated. A2 (credentials and .env files in earlier commits) and B2 (pull-request merge ratio over recent commits) read history. Under this rubric, where either would be scored, A2 abstains unless a confirmed critical secret finding stands, and B2 abstains when fewer commits are visible than it reads; each abstention counts as 0 in the composite, as any evidence Cejel expected to read and could not does. A criterion that is not applicable stays not applicable. Re-scan a full clone (for actions/checkout, set fetch-depth: 0).";
3639
+ function isShallowRepository(repoPath) {
3640
+ const result = execGit(["rev-parse", "--is-shallow-repository"], { cwd: repoPath });
3641
+ return result.ok && result.stdout.trim() === "true";
3642
+ }
3643
+ function detectHistoryLimitations(repoPath, v27Abstains = false) {
3644
+ if (!isShallowRepository(repoPath)) return void 0;
3645
+ return [v27Abstains ? SHALLOW_HISTORY_V27_ABSTENTION : SHALLOW_HISTORY_LIMITATION];
3646
+ }
2923
3647
 
2924
3648
  // src/witan/json-safe.ts
2925
3649
  init_esm_shims();
@@ -2929,7 +3653,10 @@ function stripBom(text) {
2929
3653
 
2930
3654
  // src/witan/repo-signals.ts
2931
3655
  function usesV17DetectorClosure2(rubricVersion) {
2932
- return rubricVersion === WITAN_RUBRIC_VERSION_V17 || rubricVersion === WITAN_RUBRIC_VERSION_V18 || rubricVersion === WITAN_RUBRIC_VERSION_V19 || rubricVersion === WITAN_RUBRIC_VERSION_V20 || rubricVersion === WITAN_RUBRIC_VERSION_V21 || rubricVersion === WITAN_RUBRIC_VERSION_V22 || rubricVersion === WITAN_RUBRIC_VERSION_V23 || rubricVersion === WITAN_RUBRIC_VERSION_V24 || rubricVersion === WITAN_RUBRIC_VERSION_V25 || rubricVersion === WITAN_RUBRIC_VERSION_V26;
3656
+ return rubricVersion === WITAN_RUBRIC_VERSION_V17 || rubricVersion === WITAN_RUBRIC_VERSION_V27 || rubricVersion === WITAN_RUBRIC_VERSION_V18 || rubricVersion === WITAN_RUBRIC_VERSION_V19 || rubricVersion === WITAN_RUBRIC_VERSION_V20 || rubricVersion === WITAN_RUBRIC_VERSION_V21 || rubricVersion === WITAN_RUBRIC_VERSION_V22 || rubricVersion === WITAN_RUBRIC_VERSION_V23 || rubricVersion === WITAN_RUBRIC_VERSION_V24 || rubricVersion === WITAN_RUBRIC_VERSION_V25 || rubricVersion === WITAN_RUBRIC_VERSION_V26;
3657
+ }
3658
+ function usesV27FalseAssuranceClosure(rubricVersion) {
3659
+ return rubricVersion === WITAN_RUBRIC_VERSION_V27;
2933
3660
  }
2934
3661
  var LEGACY_INVENTORY_ABSENCE_CONTEXT = {
2935
3662
  rubricGeneration: "legacy",
@@ -3029,9 +3756,16 @@ function wholesaleCriterionAbstention(signal, reasons) {
3029
3756
  metrics: [],
3030
3757
  insufficientData: true,
3031
3758
  ...insufficientDataReason !== void 0 ? { insufficientDataReason } : {},
3032
- notes: insufficientDataReason !== void 0 ? "Cejel abstained on this criterion because relevant repository content was declined under the repository content size limit or excluded from scanning by policy. This is a disclosed coverage limit, not a read failure, and does not affect the composite score." : "Cejel abstained on this criterion because one or more relevant repository files could not be read. Resolve the content-read limitations and re-scan."
3759
+ // What the collector read and set aside is still true of the content it did read. B3's names
3760
+ // workflows in a count this abstention removes, so it goes with the count.
3761
+ ...withoutCountDisclosures(signal.disclosures),
3762
+ notes: insufficientDataReason !== void 0 ? "Cejel abstained on this criterion because relevant repository content was declined under the repository content size limit or excluded from scanning by policy. This is a disclosed coverage limit, not a read failure, and does not affect the composite score." : reasons?.size === 1 && reasons.has("lfs_pointer") ? "Cejel abstained on this criterion because relevant files in this checkout are Git LFS pointers, so their content was not read. Fetch the LFS content (git lfs pull) and re-scan." : "Cejel abstained on this criterion because one or more relevant repository files could not be read. Resolve the content-read limitations and re-scan."
3033
3763
  };
3034
3764
  }
3765
+ function withoutCountDisclosures(disclosures) {
3766
+ const kept = (disclosures ?? []).filter((text) => !text.startsWith(WITAN_B3_UNSHOWN_CHECK_PREFIX));
3767
+ return kept.length > 0 ? { disclosures: kept } : {};
3768
+ }
3035
3769
  function skipReasonsForCriterion(criterionId, tracked) {
3036
3770
  const combined = new Set(tracked.criterionSkipReasons.get(criterionId) ?? []);
3037
3771
  const prefix = contentReadSignalKey(criterionId, "");
@@ -3046,15 +3780,24 @@ function skipReasonsForSignal(criterionId, signalName, tracked) {
3046
3780
  return reasons ? new Set(reasons) : void 0;
3047
3781
  }
3048
3782
  function buildWitanInputFromRepo(options) {
3049
- const tracked = trackContentReads(options.repoPath, () => buildWitanInputFromRepoUntracked(options));
3050
- const affectedCriteria = tracked.affectedCriteria;
3051
3783
  const rubricVersion = options.rubricVersion ?? WITAN_RUBRIC_VERSION;
3784
+ const tracked = trackContentReads(
3785
+ options.repoPath,
3786
+ () => buildWitanInputFromRepoUntracked(options),
3787
+ { lfsPointerAbstention: usesV27FalseAssuranceClosure(rubricVersion) }
3788
+ );
3789
+ const affectedCriteria = tracked.affectedCriteria;
3052
3790
  if (rubricVersion !== WITAN_RUBRIC_VERSION_V23) {
3053
3791
  const readFailureCriteria = [];
3054
3792
  const coverageLimitedCriteria = [];
3793
+ const lfsKept = /* @__PURE__ */ new Set();
3055
3794
  const signals2 = (tracked.value.signals ?? []).map((signal) => {
3056
3795
  if (!affectedCriteria.has(signal.criterionId)) return signal;
3057
3796
  const reasons = skipReasonsForCriterion(signal.criterionId, tracked);
3797
+ if (usesV27FalseAssuranceClosure(rubricVersion) && reasons.size > 0 && [...reasons].every((reason) => reason === "lfs_pointer") && (signal.findings ?? []).some(({ severity }) => severity === "critical")) {
3798
+ lfsKept.add(signal.criterionId);
3799
+ return signal;
3800
+ }
3058
3801
  const { insufficientDataReason } = classifyAbstentionReason(reasons);
3059
3802
  (insufficientDataReason !== void 0 ? coverageLimitedCriteria : readFailureCriteria).push(
3060
3803
  signal.criterionId
@@ -3076,7 +3819,10 @@ function buildWitanInputFromRepo(options) {
3076
3819
  ...tracked.value,
3077
3820
  signals: signals2,
3078
3821
  ...scanLimitations2.length > 0 ? { scanLimitations: scanLimitations2 } : {},
3079
- contentReadSummary: tracked.summary
3822
+ contentReadSummary: lfsKept.size > 0 ? {
3823
+ ...tracked.summary,
3824
+ affectedCriteria: tracked.summary.affectedCriteria.filter((id) => !lfsKept.has(id))
3825
+ } : tracked.summary
3080
3826
  };
3081
3827
  }
3082
3828
  const wholesaleAbstainedReadFailureCriteria = [];
@@ -3208,6 +3954,7 @@ function buildWitanInputFromRepoUntracked(options) {
3208
3954
  const usesV25InlineHealthRoute = rubricVersion === WITAN_RUBRIC_VERSION_V25 || rubricVersion === WITAN_RUBRIC_VERSION_V26;
3209
3955
  const usesV26SettlingEvidence = rubricVersion === WITAN_RUBRIC_VERSION_V26;
3210
3956
  const usesV26BuildStepEvidence = rubricVersion === WITAN_RUBRIC_VERSION_V26;
3957
+ const usesV27Closure = usesV27FalseAssuranceClosure(rubricVersion);
3211
3958
  const structuralArchetype = classifyRepoArchetype(inventoryFiles, rubricVersion);
3212
3959
  const readableArchetype = rubricVersion === WITAN_RUBRIC_VERSION_V13 || rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15 || rubricVersion === WITAN_RUBRIC_VERSION_V16 || usesV17DetectorClosure2(rubricVersion) ? applyReadableSourceRepresentationGate(
3213
3960
  options.repoPath,
@@ -3250,6 +3997,7 @@ function buildWitanInputFromRepoUntracked(options) {
3250
3997
  usesV25InlineHealthRoute,
3251
3998
  usesV26SettlingEvidence,
3252
3999
  usesV26BuildStepEvidence,
4000
+ usesV27Closure,
3253
4001
  reviewableSourceProof,
3254
4002
  inventoryAbsenceContext,
3255
4003
  scanLimitations
@@ -3587,7 +4335,7 @@ function isRepresentativeSourceText(contents) {
3587
4335
  return disallowedControls / contents.length <= 0.01;
3588
4336
  }
3589
4337
  function representativeSourceRank(rankKey, file) {
3590
- return createHash3("sha256").update(`${rankKey}:${file}`).digest("hex");
4338
+ return createHash4("sha256").update(`${rankKey}:${file}`).digest("hex");
3591
4339
  }
3592
4340
  function explainNoMeasurementSourceCoverage(repoPath, rubricVersion) {
3593
4341
  if (!usesV17DetectorClosure2(rubricVersion)) return void 0;
@@ -3839,9 +4587,9 @@ function explainIgnoredScanTarget(repoPath) {
3839
4587
  }
3840
4588
  const top = topResult.stdout.trim();
3841
4589
  if (!top) return void 0;
3842
- const topResolved = realpathSync(top);
3843
- const targetResolved = realpathSync(repoPath);
3844
- const target = relative(topResolved, targetResolved).split(sep).join("/");
4590
+ const topResolved = realpathSync2(top);
4591
+ const targetResolved = realpathSync2(repoPath);
4592
+ const target = relative2(topResolved, targetResolved).split(sep2).join("/");
3845
4593
  if (!target || target.startsWith("../")) return void 0;
3846
4594
  const ignored = execGit(["check-ignore", "--quiet", "--", `${target}/`], {
3847
4595
  cwd: topResolved
@@ -3864,7 +4612,7 @@ function buildNotApplicableSignal(criterionId, reason) {
3864
4612
  notApplicable: true
3865
4613
  };
3866
4614
  }
3867
- function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV18NativeRls, useV19CommitYear, useV20A3ExplicitGaps, useV21ExecutedEscalations, useV22PackageStartEntrypoint, useV23StrictCoverageAndRecipeRunners, useV23PemPrivateKeyGrammar, useV23WithheldPathAbstention, useV24SecretContentContext, useV25InlineHealthRoute, useV26SettlingEvidence, useV26BuildStepEvidence, reviewableSourceProof, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT, scanLimitations = /* @__PURE__ */ new Set()) {
4615
+ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV18NativeRls, useV19CommitYear, useV20A3ExplicitGaps, useV21ExecutedEscalations, useV22PackageStartEntrypoint, useV23StrictCoverageAndRecipeRunners, useV23PemPrivateKeyGrammar, useV23WithheldPathAbstention, useV24SecretContentContext, useV25InlineHealthRoute, useV26SettlingEvidence, useV26BuildStepEvidence, useV27FalseAssuranceClosure, reviewableSourceProof, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT, scanLimitations = /* @__PURE__ */ new Set()) {
3868
4616
  const signals = [];
3869
4617
  const mono = resolveMonorepoContext(repoPath, scanLimitations);
3870
4618
  const collectCriterion = (criterionId, collect) => withContentReadCriterion(criterionId, collect);
@@ -3878,7 +4626,8 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
3878
4626
  useV36Detectors,
3879
4627
  useV23StrictCoverageAndRecipeRunners,
3880
4628
  reviewableSourceProof,
3881
- inventoryAbsenceContext
4629
+ inventoryAbsenceContext,
4630
+ useV27FalseAssuranceClosure
3882
4631
  )
3883
4632
  );
3884
4633
  const a2Signal = collectCriterion(
@@ -3896,7 +4645,8 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
3896
4645
  useV18NativeRls,
3897
4646
  inventoryAbsenceContext,
3898
4647
  scanLimitations,
3899
- useV26SettlingEvidence
4648
+ useV26SettlingEvidence,
4649
+ useV27FalseAssuranceClosure
3900
4650
  )
3901
4651
  );
3902
4652
  const a3Signal = collectCriterion(
@@ -3921,7 +4671,8 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
3921
4671
  mono,
3922
4672
  useV27Detectors,
3923
4673
  useV47Detectors,
3924
- inventoryAbsenceContext
4674
+ inventoryAbsenceContext,
4675
+ useV27FalseAssuranceClosure
3925
4676
  )
3926
4677
  );
3927
4678
  const a5Signal = collectCriterion(
@@ -3939,10 +4690,13 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
3939
4690
  "B1",
3940
4691
  "Repository scans do not evaluate the dispatch-trace process dimension."
3941
4692
  );
3942
- const b2Signal = collectCriterion("B2", () => collectB2PrTraceEvidence(repoPath, repoFiles));
4693
+ const b2Signal = collectCriterion(
4694
+ "B2",
4695
+ () => collectB2PrTraceEvidence(repoPath, repoFiles, useV27FalseAssuranceClosure)
4696
+ );
3943
4697
  const b3Signal = collectCriterion(
3944
4698
  "B3",
3945
- () => collectB3CiDisciplineEvidence(repoPath, repoFiles, mono)
4699
+ () => collectB3CiDisciplineEvidence(repoPath, repoFiles, mono, useV27FalseAssuranceClosure)
3946
4700
  );
3947
4701
  const b4Signal = collectCriterion(
3948
4702
  "B4",
@@ -3984,14 +4738,16 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
3984
4738
  }
3985
4739
  return signals;
3986
4740
  }
3987
- function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV23StrictCoverageAndRecipeRunners, reviewableSourceProof, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT) {
4741
+ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV23StrictCoverageAndRecipeRunners, reviewableSourceProof, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT, useV27FalseAssuranceClosure = false) {
3988
4742
  const evidence = [];
3989
4743
  const findings = [];
3990
- const allTestFiles = findConcreteTestFiles(
4744
+ const v17TestFiles = findConcreteTestFiles(
3991
4745
  repoPath,
3992
4746
  repoFiles,
3993
4747
  reviewableSourceProof ? WITAN_RUBRIC_VERSION_V17 : void 0
3994
4748
  );
4749
+ const allTestFiles = useV27FalseAssuranceClosure ? v17TestFiles.filter((file) => !PYTEST_CONFTEST_PATTERN.test(file)) : v17TestFiles;
4750
+ const runWideSkips = useV27FalseAssuranceClosure ? findPytestRunWideSkips(repoPath, repoFiles) : [];
3995
4751
  const testFiles = allTestFiles.slice(0, 8);
3996
4752
  const sourceFiles = repoFiles.filter(isSourceFile);
3997
4753
  const runnerFiles = repoFiles.filter(
@@ -4047,13 +4803,31 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
4047
4803
  const verificationScriptCount = verificationScriptComponents.filter((c) => c.count > 0).length;
4048
4804
  const usesLeanBuiltInTestRunner = packageJson != null && [...packageScripts.values()].some(
4049
4805
  (script) => LEAN_TEST_RUNNER_SCRIPT_PATTERN.test(script)
4050
- ) || testFiles.some((file) => fileContains(repoPath, file, LEAN_TEST_RUNNER_IMPORT_PATTERN));
4806
+ ) || // v17's first eight, under every rubric: v27's conftest exclusion must not move a file into
4807
+ // this sample and change the finding below.
4808
+ v17TestFiles.slice(0, 8).some((file) => fileContains(repoPath, file, LEAN_TEST_RUNNER_IMPORT_PATTERN));
4051
4809
  const hasHeavyTestDependency = packageJsonFiles.some(
4052
4810
  (manifest) => packageJsonHasHeavyTestDependency(repoPath, manifest)
4053
4811
  );
4054
4812
  const isLeanTestToolchain = usesLeanBuiltInTestRunner && !hasHeavyTestDependency;
4055
4813
  for (const file of testFiles) {
4056
- evidence.push(evidenceForRelative(repoPath, file, "test_run", "Detected test file"));
4814
+ const testPath = join(repoPath, file);
4815
+ const isLfsPointer = isRegularFile(testPath) && isGitLfsPointerText(readRepoText(testPath, "utf8"));
4816
+ evidence.push(
4817
+ evidenceForRelative(
4818
+ repoPath,
4819
+ file,
4820
+ "test_run",
4821
+ isLfsPointer ? "Detected test file (Git LFS pointer; its content is not in this checkout)" : "Detected test file"
4822
+ )
4823
+ );
4824
+ }
4825
+ if (useV27FalseAssuranceClosure && allTestFiles.length === 0) {
4826
+ for (const file of v17TestFiles.slice(0, 8)) {
4827
+ evidence.push(
4828
+ evidenceForRelative(repoPath, file, "test_run", "pytest configuration (conftest.py), not a test file")
4829
+ );
4830
+ }
4057
4831
  }
4058
4832
  for (const file of runnerFiles) {
4059
4833
  evidence.push(evidenceForRelative(repoPath, file, "test_run", "Configured test runner"));
@@ -4072,7 +4846,7 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
4072
4846
  );
4073
4847
  }
4074
4848
  const scheduledHealthWorkflow = ciWorkflows.find(
4075
- (file) => fileContains(repoPath, file, SCHEDULE_TRIGGER_PATTERN) && fileContains(repoPath, file, CI_TEST_COMMAND_PATTERN) && (!useV36Detectors || SCHEDULED_HEALTH_PATH_PATTERN.test(file) || fileContains(repoPath, file, SCHEDULED_HEALTH_INTENT_PATTERN))
4849
+ (file) => fileSatisfies(repoPath, file, hasScheduleCronTrigger) && fileContains(repoPath, file, CI_TEST_COMMAND_PATTERN) && (!useV36Detectors || SCHEDULED_HEALTH_PATH_PATTERN.test(file) || fileContains(repoPath, file, SCHEDULED_HEALTH_INTENT_PATTERN))
4076
4850
  );
4077
4851
  if (scheduledHealthWorkflow) {
4078
4852
  const isPublished = fileContains(
@@ -4173,8 +4947,30 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
4173
4947
  const nonHollowShare = withContentReadSignal(
4174
4948
  "A1",
4175
4949
  "non_hollow_test_share",
4176
- () => measureNonHollowTestShare(repoPath, allTestFiles, reviewableSourceProof !== void 0)
4950
+ () => measureNonHollowTestShare(
4951
+ repoPath,
4952
+ v17TestFiles,
4953
+ reviewableSourceProof !== void 0,
4954
+ useV27FalseAssuranceClosure ? {
4955
+ disabledByRunWideSkip: (file) => runWideSkips.some((skip) => isGovernedPytestFile(skip, file))
4956
+ } : void 0
4957
+ )
4177
4958
  );
4959
+ for (const skip of runWideSkips.slice(0, 2)) {
4960
+ const governed = allTestFiles.filter((file) => isGovernedPytestFile(skip, file)).length;
4961
+ if (governed === 0) continue;
4962
+ findings.push({
4963
+ severity: "warning",
4964
+ summary: `A pytest collection hook in ${skip.path} ${skip.kind === "skip" ? "marks every collected test skipped" : "removes every collected test"}, so the ${governed} pytest file${governed === 1 ? "" : "s"} under ${skip.scope === "" ? "the repository root" : `${skip.scope}/`} cannot run.`,
4965
+ evidence: evidenceForRelativeAtLine(
4966
+ repoPath,
4967
+ skip.path,
4968
+ "test_run",
4969
+ "Run-wide pytest skip hook",
4970
+ skip.line
4971
+ )
4972
+ });
4973
+ }
4178
4974
  return {
4179
4975
  criterionId: "A1",
4180
4976
  positiveEvidence: evidence,
@@ -4311,7 +5107,7 @@ function buildA1AuthenticatedAbsenceSignal(repoPath, proof, inventoryCount, inve
4311
5107
  notes: `V17 authenticated absence: ${proof.eligibleSourceFileCount} criterion-ratable of ${proof.sourceShapedFileCount} source-shaped files; sampled ${proof.sampledFileCount}; ${proof.readableSampledFileCount} representative-text files. Verification-script signals (lint/typecheck/coverage/CI commands) are independent of test-file presence and are still credited above where detected.`
4312
5108
  };
4313
5109
  }
4314
- function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV23PemPrivateKeyGrammar, useV24SecretContentContext, useV18NativeRls, inventoryAbsenceContext, scanLimitations, useV26SettlingEvidence = false) {
5110
+ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV23PemPrivateKeyGrammar, useV24SecretContentContext, useV18NativeRls, inventoryAbsenceContext, scanLimitations, useV26SettlingEvidence = false, useV27FalseAssuranceClosure = false) {
4315
5111
  const evidence = [];
4316
5112
  const findings = [];
4317
5113
  const gitignore = repoFiles.find((file) => basename(file) === ".gitignore");
@@ -4319,22 +5115,26 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
4319
5115
  const migrationFiles = repoFiles.filter(
4320
5116
  (file) => /(^|\/)(migrations?|drizzle|prisma)\//.test(file)
4321
5117
  );
4322
- const { committedSecret, ambiguousSecret, currentSecretFingerprintsByPath } = withContentReadSignal("A2", "secret_cleanliness", () => {
5118
+ const { committedSecret, ambiguousSecret, currentSecretFingerprintsByPath, placeholderSetAside } = withContentReadSignal("A2", "secret_cleanliness", () => {
4323
5119
  let match_;
4324
5120
  let ambiguous_;
5121
+ const setAsideFiles = [];
4325
5122
  for (const path of repoFiles) {
4326
5123
  const authoredProductionPath = useV24SecretContentContext ? isAuthoredProductionPath(path) : useV47Detectors ? isV47AuthoredProductionPath(path) : useV39Detectors ? isV39AuthoredProductionPath(path) : isAuthoredProductionPath(path);
4327
5124
  if ((useV24SecretContentContext ? isV24IgnoredScanFile(path) : isIgnoredScanFile(path, useV47Detectors)) || useV33Detectors && !authoredProductionPath) {
4328
5125
  continue;
4329
5126
  }
5127
+ const setAside = { count: 0 };
4330
5128
  if (useV24SecretContentContext) {
4331
5129
  const classified = findV24ClassifiedSecretInFile(
4332
5130
  repoPath,
4333
5131
  path,
4334
5132
  useV36Detectors,
4335
5133
  useV39Detectors,
4336
- useV47Detectors
5134
+ useV47Detectors,
5135
+ setAside
4337
5136
  );
5137
+ if (setAside.count > 0) setAsideFiles.push({ path, count: setAside.count });
4338
5138
  if (!classified) continue;
4339
5139
  if (classified.classification === "real") {
4340
5140
  match_ = { path, match: classified.match };
@@ -4349,8 +5149,10 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
4349
5149
  useV36Detectors,
4350
5150
  useV39Detectors,
4351
5151
  useV47Detectors,
4352
- useV23PemPrivateKeyGrammar
5152
+ useV23PemPrivateKeyGrammar,
5153
+ setAside
4353
5154
  );
5155
+ if (setAside.count > 0) setAsideFiles.push({ path, count: setAside.count });
4354
5156
  if (!found) continue;
4355
5157
  match_ = { path, match: found };
4356
5158
  break;
@@ -4381,7 +5183,10 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
4381
5183
  // abstention exists for repositories Cejel could not classify, not as a way for one
4382
5184
  // unclassifiable value to withhold a metric a confirmed leak already decided.
4383
5185
  ambiguousSecret: match_ ? void 0 : ambiguous_,
4384
- currentSecretFingerprintsByPath: fingerprintsByPath
5186
+ currentSecretFingerprintsByPath: fingerprintsByPath,
5187
+ // Only a scan that read every eligible file can say what it set aside; one that stopped at
5188
+ // a confirmed secret did not, and the certificate already reports that secret.
5189
+ placeholderSetAside: match_ ? [] : setAsideFiles
4385
5190
  };
4386
5191
  });
4387
5192
  const historySecretScan = committedSecret && !useV27Detectors ? null : collectHistorySecretEvidence(
@@ -4415,10 +5220,13 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
4415
5220
  const hasDataLayer = migrationFiles.length > 0 || dbLayerImportFile !== void 0;
4416
5221
  const hasSecretsSurface = envExamples.length > 0 || committedEnvFilePath !== void 0 || gitignoreHasEnvRule || hasEnvInHistory;
4417
5222
  if (!hasDataLayer && !hasSecretsSurface && !hasConfirmedSecretFinding && !cryptoHygiene.hasSurface) {
4418
- return buildNotApplicableSignal(
4419
- "A2",
4420
- "No data layer (DB/ORM/migrations) or ratable secrets surface detected \u2014 A2 not applicable to this repo archetype. A ratable surface requires .env* files, .gitignore .env rule, committed/history .env path, or detected signing/HMAC/secret-comparison code; bare env reads (process.env / os.environ / std::env::) do not qualify."
4421
- );
5223
+ return {
5224
+ ...buildNotApplicableSignal(
5225
+ "A2",
5226
+ "No data layer (DB/ORM/migrations) or ratable secrets surface detected \u2014 A2 not applicable to this repo archetype. A ratable surface requires .env* files, .gitignore .env rule, committed/history .env path, or detected signing/HMAC/secret-comparison code; bare env reads (process.env / os.environ / std::env::) do not qualify."
5227
+ ),
5228
+ ...placeholderSetAsideDisclosures2(placeholderSetAside)
5229
+ };
4422
5230
  }
4423
5231
  if (committedEnvFilePath) {
4424
5232
  evidence.push(
@@ -4485,7 +5293,12 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
4485
5293
  "A2",
4486
5294
  "env_handling_depth",
4487
5295
  () => evidence.push(
4488
- evidenceForRelative(repoPath, gitignoreEnvFile, "secret_scan", ".env files are gitignored")
5296
+ evidenceForRelative(
5297
+ repoPath,
5298
+ gitignoreEnvFile,
5299
+ "secret_scan",
5300
+ gitignoreEnvRuleLabel(repoFiles.filter(isCommittedEnvFormatPath))
5301
+ )
4489
5302
  )
4490
5303
  );
4491
5304
  }
@@ -4649,6 +5462,9 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
4649
5462
  findings.push(...cryptoHygiene.findings);
4650
5463
  if (evidence.length === 0 && findings.length === 0) return null;
4651
5464
  const hasCriticalFinding = findings.some((finding) => finding.severity === "critical");
5465
+ if (useV27FalseAssuranceClosure && !hasCriticalFinding && isShallowRepository(repoPath)) {
5466
+ return shallowHistoryAbstention("A2", V27_A2_SHALLOW_HISTORY_NOTE);
5467
+ }
4652
5468
  const secretCleanliness = hasCriticalFinding ? 0 : 1;
4653
5469
  const abstainsSecretCleanliness = useV24SecretContentContext && ambiguousSecret != null && !hasCriticalFinding;
4654
5470
  const baseMetrics = [
@@ -4660,7 +5476,9 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
4660
5476
  1,
4661
5477
  isMultiTenant ? 0.25 : 0.7,
4662
5478
  "clean",
4663
- "Credits absence of committed or recent-history secret findings in the static scan."
5479
+ "Credits absence of committed or recent-history secret findings in the static scan.",
5480
+ void 0,
5481
+ secretCleanliness === 1 && placeholderSetAside.length > 0 ? { condition: placeholderSetAsideCondition(placeholderSetAside) } : void 0
4664
5482
  )
4665
5483
  ],
4666
5484
  metric(
@@ -4734,9 +5552,25 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
4734
5552
  "Cejel abstained on secret cleanliness: the current-tree scan matched a secret-shaped value it could not classify from content as either a credential or an example. The metric is withheld rather than scored; every other A2 signal is unaffected."
4735
5553
  ] : [],
4736
5554
  ...tenantIsolationUnsettled ? [WITAN_A2_TENANT_ISOLATION_UNSETTLED_DISCLOSURE] : []
4737
- ].join(" ")
5555
+ ].join(" "),
5556
+ ...placeholderSetAsideDisclosures2(placeholderSetAside)
4738
5557
  };
4739
5558
  }
5559
+ function isCommittedEnvFormatPath(path) {
5560
+ const name = basename(path);
5561
+ return /^\.env(?:\..+)?$|.\.env$/i.test(name) && !isEnvTemplatePath(path, true) && !/(?:example|sample|template)/i.test(name);
5562
+ }
5563
+ function gitignoreEnvRuleLabel(committedEnvFiles) {
5564
+ const count = committedEnvFiles.length;
5565
+ if (count === 0) return ".env files are gitignored";
5566
+ const lead = `.gitignore has an .env rule, but the scanned tree includes ${count} env file${count === 1 ? "" : "s"}`;
5567
+ for (let shown = Math.min(count, 2); shown > 0; shown -= 1) {
5568
+ const more = count - shown;
5569
+ const label = `${lead}: ${committedEnvFiles.slice(0, shown).join(", ")}${more > 0 ? ` and ${more} more` : ""}`;
5570
+ if (label.length <= EVIDENCE_LABEL_MAX_LENGTH) return label;
5571
+ }
5572
+ return lead;
5573
+ }
4740
5574
  function collectCryptoHygieneEvidence(repoPath, implFiles, useV39Detectors = false) {
4741
5575
  const evidence = [];
4742
5576
  const findings = [];
@@ -5210,7 +6044,7 @@ function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, us
5210
6044
  ...buildStepSettlement === "unsettled" ? { notes: WITAN_A3_BUILD_STEP_UNSETTLED_DISCLOSURE } : {}
5211
6045
  };
5212
6046
  }
5213
- function collectA4DependencyEvidence(repoPath, repoFiles, mono, useV27Detectors, useV47Detectors, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT) {
6047
+ function collectA4DependencyEvidence(repoPath, repoFiles, mono, useV27Detectors, useV47Detectors, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT, useV27FalseAssuranceClosure = false) {
5214
6048
  const evidence = [];
5215
6049
  const findings = [];
5216
6050
  const manifests = repoFiles.filter(isDependencyManifest);
@@ -5323,6 +6157,62 @@ function collectA4DependencyEvidence(repoPath, repoFiles, mono, useV27Detectors,
5323
6157
  )
5324
6158
  });
5325
6159
  }
6160
+ const checkoutPaths = [...repoFiles, ...withheldRepoPaths().map(({ path }) => path)];
6161
+ const knownPaths = [...checkoutPaths, ...mono?.sharedFiles ?? []];
6162
+ const knownBasenames = new Set(knownPaths.map((path) => basename(path)));
6163
+ const findRequirement = () => findLockfileRequirement(
6164
+ repoPath,
6165
+ repoFiles.filter(isGitHubActionsWorkflow),
6166
+ checkoutInventory(checkoutPaths),
6167
+ (lockfiles2) => lockfiles2.some((name) => knownBasenames.has(name))
6168
+ );
6169
+ const lockfileRequirement = hasLockfile || !manifest ? void 0 : useV27FalseAssuranceClosure ? withContentReadSignal("A4", "lockfile_coverage", findRequirement) : withoutContentReadAttribution(findRequirement);
6170
+ const leadingEvidence = [];
6171
+ if (lockfileRequirement) {
6172
+ const cite = () => evidenceForRelativeAtLine(
6173
+ repoPath,
6174
+ lockfileRequirement.file,
6175
+ "dependency_report",
6176
+ `CI step runs \`${lockfileRequirement.command}\`, which needs ${joinLockfileNames(lockfileRequirement.lockfiles)}; the scanned inventory holds none`,
6177
+ lockfileRequirement.line
6178
+ );
6179
+ leadingEvidence.push(
6180
+ useV27FalseAssuranceClosure ? withContentReadSignal("A4", "lockfile_coverage", cite) : withoutContentReadAttribution(cite)
6181
+ );
6182
+ }
6183
+ if (!isAppOrService && manifest) {
6184
+ const unconstrained = withContentReadSignals(
6185
+ "A4",
6186
+ ["pinned_dependency_ratio", "declared_version_range_ratio", "dependency_count_sanity"],
6187
+ () => {
6188
+ const unconstrainedCount = (file) => readDependencySpecs(repoPath, [file]).filter(
6189
+ (dependency) => !hasDeclaredVersionConstraint(dependency)
6190
+ ).length;
6191
+ if (unconstrainedCount(manifest) > 0) return void 0;
6192
+ for (const file of manifests) {
6193
+ if (file === manifest) continue;
6194
+ const count = unconstrainedCount(file);
6195
+ if (count > 0) return { file, count };
6196
+ }
6197
+ return void 0;
6198
+ }
6199
+ );
6200
+ if (unconstrained) {
6201
+ leadingEvidence.push(
6202
+ withContentReadSignals(
6203
+ "A4",
6204
+ ["pinned_dependency_ratio", "declared_version_range_ratio", "dependency_count_sanity"],
6205
+ () => evidenceForRelative(
6206
+ repoPath,
6207
+ unconstrained.file,
6208
+ "dependency_report",
6209
+ `Dependency manifest with ${unconstrained.count} ${unconstrained.count === 1 ? "dependency" : "dependencies"} declared without a version constraint`
6210
+ )
6211
+ )
6212
+ );
6213
+ }
6214
+ }
6215
+ evidence.unshift(...leadingEvidence);
5326
6216
  const automationMetric = metric(
5327
6217
  "dependency_automation_ratio",
5328
6218
  "Dependency automation ratio",
@@ -5383,6 +6273,18 @@ function collectA4DependencyEvidence(repoPath, repoFiles, mono, useV27Detectors,
5383
6273
  "present",
5384
6274
  "Credited when present (reproducible dev/CI installs); a library without a committed lockfile is not penalized \u2014 consumers resolve their own trees."
5385
6275
  )
6276
+ ] : useV27FalseAssuranceClosure && lockfileRequirement ? [
6277
+ // v27 (#520): the repository's own CI requires a lockfile, so its absence is not the
6278
+ // library norm. A zero term can only lower the weighted mean.
6279
+ metric(
6280
+ "lockfile_coverage",
6281
+ "Lockfile coverage",
6282
+ 0,
6283
+ 1,
6284
+ 0.3,
6285
+ "present",
6286
+ "Scored 0: this repository's own CI requires a lockfile, and the scanned inventory holds no recognized lockfile."
6287
+ )
5386
6288
  ] : [],
5387
6289
  automationMetric,
5388
6290
  metric(
@@ -5400,8 +6302,373 @@ function collectA4DependencyEvidence(repoPath, repoFiles, mono, useV27Detectors,
5400
6302
  positiveEvidence: evidence,
5401
6303
  findings,
5402
6304
  metrics: isAppOrService ? appMetrics : libraryMetrics,
5403
- notes: isAppOrService ? `A4 scored against app/service norms (${packagedApplicationFiles.length > 0 ? "packaged application" : "deploy surface"} detected): pinned dependencies and a lockfile are required for reproducible installs.` : "A4 scored against library/CLI norms (no deploy surface detected \u2014 same archetype line as A3): declared version ranges are correct library behavior; a committed lockfile is credited but not required."
6305
+ notes: isAppOrService ? `A4 scored against app/service norms (${packagedApplicationFiles.length > 0 ? "packaged application" : "deploy surface"} detected): pinned dependencies and a lockfile are required for reproducible installs.` : lockfileRequirement ? `A4 scored against library/CLI norms (no deploy surface detected \u2014 same archetype line as A3): declared version ranges are correct library behavior. An unconditional step in this repository's CI runs \`${lockfileRequirement.command}\` (${lockfileRequirement.file} line ${lockfileRequirement.line}), which exits with an error without ${joinLockfileNames(lockfileRequirement.lockfiles)}, and the scanned inventory holds none.`.slice(0, 1e3) : "A4 scored against library/CLI norms (no deploy surface detected \u2014 same archetype line as A3): declared version ranges are correct library behavior; a committed lockfile is credited but not required."
6306
+ };
6307
+ }
6308
+ var LOCKFILE_REQUIRING_COMMANDS = [
6309
+ {
6310
+ tool: "npm",
6311
+ subcommands: ["ci"],
6312
+ flags: null,
6313
+ lockfiles: ["package-lock.json", "npm-shrinkwrap.json"],
6314
+ generators: ["install", "i", "add", "update", "up"],
6315
+ directoryFlags: ["--prefix", "-C"],
6316
+ manifestFlags: []
6317
+ },
6318
+ {
6319
+ tool: "pnpm",
6320
+ subcommands: ["install", "i"],
6321
+ flags: ["--frozen-lockfile"],
6322
+ lockfiles: ["pnpm-lock.yaml"],
6323
+ generators: ["install", "i", "add", "update", "up"],
6324
+ directoryFlags: ["--dir", "-C"],
6325
+ manifestFlags: []
6326
+ },
6327
+ {
6328
+ tool: "uv",
6329
+ subcommands: ["run", "sync"],
6330
+ flags: ["--locked", "--frozen"],
6331
+ lockfiles: ["uv.lock"],
6332
+ generators: ["run", "sync", "lock", "add"],
6333
+ directoryFlags: ["--directory", "--project"],
6334
+ manifestFlags: []
6335
+ },
6336
+ {
6337
+ tool: "cargo",
6338
+ subcommands: ["build", "test", "check", "clippy", "doc", "run", "bench"],
6339
+ flags: ["--locked", "--frozen"],
6340
+ lockfiles: ["Cargo.lock"],
6341
+ generators: ["build", "test", "check", "clippy", "doc", "run", "bench", "generate-lockfile", "update"],
6342
+ directoryFlags: ["-C"],
6343
+ manifestFlags: ["--manifest-path"]
6344
+ },
6345
+ {
6346
+ tool: "poetry",
6347
+ subcommands: ["check"],
6348
+ flags: ["--lock"],
6349
+ lockfiles: ["poetry.lock"],
6350
+ generators: ["lock", "install", "add", "update"],
6351
+ directoryFlags: ["--directory", "-C", "--project", "-P"],
6352
+ manifestFlags: []
6353
+ }
6354
+ ];
6355
+ var CONDITIONAL_SHELL_WORDS = /* @__PURE__ */ new Set([
6356
+ "if",
6357
+ "then",
6358
+ "else",
6359
+ "elif",
6360
+ "fi",
6361
+ "case",
6362
+ "esac",
6363
+ "[",
6364
+ "[[",
6365
+ "test",
6366
+ "while",
6367
+ "until",
6368
+ "for",
6369
+ "do",
6370
+ "done"
6371
+ ]);
6372
+ var SHELL_COMMAND_SEPARATORS = /* @__PURE__ */ new Set(["&&", "||", ";", "|"]);
6373
+ var DIRECTORY_CHANGE_COMMANDS = /* @__PURE__ */ new Set(["cd", "pushd"]);
6374
+ function joinLockfileNames(names) {
6375
+ return names.length === 1 ? names[0] ?? "" : `${names.slice(0, -1).join(", ")} or ${names[names.length - 1]}`;
6376
+ }
6377
+ function isGitHubActionsWorkflow(file) {
6378
+ return /^\.github\/workflows\/[^/]+\.ya?ml$/.test(file);
6379
+ }
6380
+ function beforeShellComment(line) {
6381
+ const trimmed = line.trimStart();
6382
+ if (trimmed.startsWith("#")) return "";
6383
+ const cut = [trimmed.indexOf(" #"), trimmed.indexOf(" #")].filter((index) => index !== -1);
6384
+ return cut.length > 0 ? trimmed.slice(0, Math.min(...cut)) : trimmed;
6385
+ }
6386
+ function shellCommands(line) {
6387
+ const commands = [[]];
6388
+ for (const raw of beforeShellComment(line).split(/[ \t]+/)) {
6389
+ const word = raw.endsWith(";") && raw.length > 1 ? raw.slice(0, -1) : raw;
6390
+ if (word === "") continue;
6391
+ if (SHELL_COMMAND_SEPARATORS.has(word)) {
6392
+ commands.push([]);
6393
+ continue;
6394
+ }
6395
+ commands[commands.length - 1]?.push(word);
6396
+ if (raw.endsWith(";") && raw.length > 1) commands.push([]);
6397
+ }
6398
+ return commands.filter((command) => command.length > 0);
6399
+ }
6400
+ var OMITTED_COMMAND_WORDS_MARKER = "\u2026";
6401
+ function matchLockfileRequiringCommand(words, rule) {
6402
+ if (words[0] !== rule.tool) return void 0;
6403
+ const subcommand = rule.subcommands.find((candidate) => candidate === words[1]);
6404
+ if (subcommand === void 0) return void 0;
6405
+ if (rule.flags === null) return `${rule.tool} ${subcommand}`;
6406
+ const flags = rule.flags;
6407
+ const end = words.indexOf("--");
6408
+ const own = end === -1 ? words.slice(2) : words.slice(2, end);
6409
+ for (const [index, word] of own.entries()) {
6410
+ const flag = flags.find((candidate) => candidate === (word.split("=")[0] ?? word));
6411
+ if (flag === void 0) continue;
6412
+ return `${rule.tool} ${subcommand}${index > 0 ? ` ${OMITTED_COMMAND_WORDS_MARKER}` : ""} ${flag}`;
6413
+ }
6414
+ return void 0;
6415
+ }
6416
+ function commandHead(words) {
6417
+ let inverted = false;
6418
+ for (let index = 0; index < words.length; index += 1) {
6419
+ const raw = words[index] ?? "";
6420
+ let lead = 0;
6421
+ while (lead < raw.length && (raw[lead] === "(" || raw[lead] === "{" || raw[lead] === "!")) lead += 1;
6422
+ const prefix = raw.slice(0, lead);
6423
+ if (prefix.includes("!") || prefix.includes("((")) inverted = true;
6424
+ if (lead < raw.length) return { word: raw.slice(lead), argsFrom: index + 1, inverted };
6425
+ }
6426
+ return { word: "", argsFrom: words.length, inverted };
6427
+ }
6428
+ function isConditionalShellCommand(words) {
6429
+ const head = commandHead(words);
6430
+ if (head.inverted || CONDITIONAL_SHELL_WORDS.has(head.word)) return true;
6431
+ if (head.word !== "set") return false;
6432
+ const args = words.slice(head.argsFrom);
6433
+ return args.some(
6434
+ (arg, index) => arg === "+o" && args[index + 1] === "errexit" || arg.length > 1 && arg.startsWith("+") && /^[A-Za-z]+$/.test(arg.slice(1)) && arg.includes("e")
6435
+ );
6436
+ }
6437
+ var RUN_KEY_PATTERN = /^([ \t]*)(?:-[ \t]+)?run:[ \t]*(.*)$/;
6438
+ var IF_KEY_PATTERN = /^[ \t]*(?:-[ \t]+)?if:/;
6439
+ var CONTINUE_ON_ERROR_KEY_PATTERN = /^[ \t]*(?:-[ \t]+)?continue-on-error:[ \t]*(.*)$/;
6440
+ var STEPS_KEY_PATTERN = /^[ \t]*steps:[ \t]*$/;
6441
+ var LIST_ITEM_PATTERN = /^[ \t]*-[ \t]/;
6442
+ var BLOCK_SCALAR_KEY_PATTERN = /^[ \t]*(?:-[ \t]+)?[^\s:#'"-][^:]*:[ \t]*[|>][-+0-9]*(?:[ \t]+(?:#.*)?)?$/;
6443
+ var BLOCK_SCALAR_VALUE_PATTERN = /^[|>][-+0-9]*$/;
6444
+ function lineIndent(line) {
6445
+ return line.length - line.trimStart().length;
6446
+ }
6447
+ function isStepOrJobGuard(line) {
6448
+ if (IF_KEY_PATTERN.test(line)) return true;
6449
+ const continueOnError = CONTINUE_ON_ERROR_KEY_PATTERN.exec(line);
6450
+ if (!continueOnError) return false;
6451
+ const value = beforeShellComment(continueOnError[1] ?? "").trim().replace(/^(['"])(.*)\1$/, "$2");
6452
+ return value !== "false";
6453
+ }
6454
+ function workflowKeyIndent(line, indent) {
6455
+ return indent + (line.trimStart().startsWith("-") ? 2 : 0);
6456
+ }
6457
+ function blockScalarBodyLines(lines, indents) {
6458
+ const body = new Array(lines.length).fill(false);
6459
+ for (let index = 0; index < lines.length; index += 1) {
6460
+ const raw = lines[index] ?? "";
6461
+ if (!BLOCK_SCALAR_KEY_PATTERN.test(raw)) continue;
6462
+ const keyIndent = workflowKeyIndent(raw, indents[index] ?? 0);
6463
+ let next = index + 1;
6464
+ for (; next < lines.length; next += 1) {
6465
+ if ((lines[next] ?? "").trim() !== "" && (indents[next] ?? 0) <= keyIndent) break;
6466
+ body[next] = true;
6467
+ }
6468
+ index = next - 1;
6469
+ }
6470
+ return body;
6471
+ }
6472
+ function nextAtOrBelowIndexes(structural, indents) {
6473
+ const next = new Array(structural.length).fill(structural.length);
6474
+ const open = [];
6475
+ for (let index = 0; index < structural.length; index += 1) {
6476
+ if (!structural[index]) continue;
6477
+ const indent = indents[index] ?? 0;
6478
+ while (open.length > 0 && (indents[open[open.length - 1] ?? 0] ?? 0) >= indent) {
6479
+ next[open.pop() ?? 0] = index;
6480
+ }
6481
+ open.push(index);
6482
+ }
6483
+ return next;
6484
+ }
6485
+ function leastGuardIndentWithin(structural, indents, isGuard) {
6486
+ const least = new Array(structural.length).fill(Number.POSITIVE_INFINITY);
6487
+ const open = [];
6488
+ const close = () => {
6489
+ const top = open.pop() ?? 0;
6490
+ const below = open[open.length - 1];
6491
+ if (below !== void 0) least[below] = Math.min(least[below] ?? 0, least[top] ?? 0);
5404
6492
  };
6493
+ for (let index = 0; index < structural.length; index += 1) {
6494
+ if (!structural[index]) continue;
6495
+ const indent = indents[index] ?? 0;
6496
+ while (open.length > 0 && (indents[open[open.length - 1] ?? 0] ?? 0) >= indent) close();
6497
+ if (isGuard[index]) least[index] = indent;
6498
+ open.push(index);
6499
+ }
6500
+ while (open.length > 0) close();
6501
+ return least;
6502
+ }
6503
+ function githubActionsRunSteps(contents) {
6504
+ const lines = contents.split(/\r?\n/);
6505
+ const indents = lines.map(lineIndent);
6506
+ const inBlockScalar = blockScalarBodyLines(lines, indents);
6507
+ const structural = lines.map((raw, index) => !inBlockScalar[index] && raw.trim() !== "");
6508
+ const isGuard = lines.map((raw, index) => (structural[index] ?? false) && isStepOrJobGuard(raw));
6509
+ const itemAtOrAbove = new Array(lines.length);
6510
+ const stepsKeyAbove = new Array(lines.length);
6511
+ const lessIndentedAbove = new Array(lines.length);
6512
+ {
6513
+ let lastItem = -1;
6514
+ let lastStepsKey = -1;
6515
+ const open = [];
6516
+ for (let index = 0; index < lines.length; index += 1) {
6517
+ const raw = lines[index] ?? "";
6518
+ if (structural[index] && LIST_ITEM_PATTERN.test(raw)) lastItem = index;
6519
+ itemAtOrAbove[index] = lastItem;
6520
+ stepsKeyAbove[index] = lastStepsKey;
6521
+ if (!structural[index]) {
6522
+ lessIndentedAbove[index] = -1;
6523
+ continue;
6524
+ }
6525
+ if (STEPS_KEY_PATTERN.test(raw)) lastStepsKey = index;
6526
+ const indent = indents[index] ?? 0;
6527
+ while (open.length > 0 && (indents[open[open.length - 1] ?? 0] ?? 0) >= indent) open.pop();
6528
+ lessIndentedAbove[index] = open.length > 0 ? open[open.length - 1] ?? -1 : -1;
6529
+ open.push(index);
6530
+ }
6531
+ }
6532
+ const nextAtOrBelow = nextAtOrBelowIndexes(structural, indents);
6533
+ const leastGuardIndent = leastGuardIndentWithin(structural, indents, isGuard);
6534
+ const guardedJobs = /* @__PURE__ */ new Set();
6535
+ for (let index = 0; index < lines.length; index += 1) {
6536
+ if (isGuard[index]) guardedJobs.add(`${lessIndentedAbove[index] ?? -1}:${indents[index] ?? 0}`);
6537
+ }
6538
+ const steps = [];
6539
+ for (let index = 0; index < lines.length; index += 1) {
6540
+ if (!structural[index]) continue;
6541
+ const raw = lines[index] ?? "";
6542
+ const match = execOnTerminatorFreeLine(RUN_KEY_PATTERN, raw);
6543
+ if (!match) continue;
6544
+ const keyIndent = workflowKeyIndent(raw, (match[1] ?? "").length);
6545
+ const value = (match[2] ?? "").trim();
6546
+ const body = [];
6547
+ if (BLOCK_SCALAR_VALUE_PATTERN.test(beforeShellComment(value).trim())) {
6548
+ for (let next = index + 1; next < lines.length && inBlockScalar[next]; next += 1) {
6549
+ const text = lines[next] ?? "";
6550
+ if (text.trim() !== "") body.push({ line: next + 1, text });
6551
+ }
6552
+ } else if (value !== "") {
6553
+ body.push({ line: index + 1, text: value.replace(/^(['"])(.*)\1$/, "$2") });
6554
+ }
6555
+ const stepStart = itemAtOrAbove[index] ?? -1;
6556
+ if (stepStart === -1) {
6557
+ steps.push({ lines: body, conditional: true });
6558
+ continue;
6559
+ }
6560
+ const placed = stepStart === index || (nextAtOrBelow[stepStart] ?? lines.length) > index;
6561
+ const stepConditional = (leastGuardIndent[stepStart] ?? Number.POSITIVE_INFINITY) <= keyIndent;
6562
+ const stepsKey = stepsKeyAbove[stepStart] ?? -1;
6563
+ const jobConditional = stepsKey !== -1 && guardedJobs.has(`${lessIndentedAbove[stepsKey] ?? -1}:${indents[stepsKey] ?? 0}`);
6564
+ steps.push({ lines: body, conditional: !placed || stepConditional || jobConditional });
6565
+ }
6566
+ return steps;
6567
+ }
6568
+ var WORKFLOW_OTHER_REPOSITORY_PATTERN = /^[ \t]*(?:-[ \t]+)?repository:[ \t]*\S/;
6569
+ var WORKFLOW_DOWNLOAD_ARTIFACT_PATTERN = /^[ \t]*(?:-[ \t]+)?uses:[ \t]*['"]?actions\/download-artifact@/;
6570
+ var WORKFLOW_WORKING_DIRECTORY_PATTERN = /^[ \t]*(?:-[ \t]+)?working-directory:[ \t]*(.*)$/;
6571
+ var EXPANDED_PATH_PATTERN = /[$~`*?[\]{}'"\\]/;
6572
+ function checkoutInventory(paths) {
6573
+ const directories = /* @__PURE__ */ new Set();
6574
+ for (const path of paths) {
6575
+ for (let end = path.lastIndexOf("/"); end > 0; end = path.lastIndexOf("/", end - 1)) {
6576
+ const directory = path.slice(0, end);
6577
+ if (directories.has(directory)) break;
6578
+ directories.add(directory);
6579
+ }
6580
+ }
6581
+ return { files: new Set(paths), directories };
6582
+ }
6583
+ function literalCheckoutPath(written) {
6584
+ let end = written.length;
6585
+ while (end > 1 && written[end - 1] === "/") end -= 1;
6586
+ let path = written.slice(0, end);
6587
+ while (path.startsWith("./")) path = path.slice(2);
6588
+ if (path === "" || path.startsWith("/") || EXPANDED_PATH_PATTERN.test(path)) return void 0;
6589
+ return path;
6590
+ }
6591
+ function isCheckoutDirectory(inventory, written) {
6592
+ const path = literalCheckoutPath(written);
6593
+ return path !== void 0 && (path === "." || inventory.directories.has(path));
6594
+ }
6595
+ function isCheckoutFile(inventory, written) {
6596
+ const path = literalCheckoutPath(written);
6597
+ return path !== void 0 && inventory.files.has(path);
6598
+ }
6599
+ function commandStaysInCheckout(words, rule, inventory) {
6600
+ const end = words.indexOf("--");
6601
+ const own = end === -1 ? words.slice(2) : words.slice(2, end);
6602
+ const checks = [
6603
+ [rule.directoryFlags, isCheckoutDirectory],
6604
+ [rule.manifestFlags, isCheckoutFile]
6605
+ ];
6606
+ for (const [index, word] of own.entries()) {
6607
+ for (const [flags, holds] of checks) {
6608
+ for (const flag of flags) {
6609
+ let value;
6610
+ if (word === flag) value = own[index + 1] ?? "";
6611
+ else if (word.startsWith(`${flag}=`)) value = word.slice(flag.length + 1);
6612
+ else if (!flag.startsWith("--") && word.startsWith(flag)) value = word.slice(flag.length);
6613
+ else continue;
6614
+ if (!holds(inventory, value)) return false;
6615
+ }
6616
+ }
6617
+ }
6618
+ return true;
6619
+ }
6620
+ function workflowReadsOnlyThisTree(inventory, contents) {
6621
+ for (const raw of contents.split(/\r?\n/)) {
6622
+ if (WORKFLOW_OTHER_REPOSITORY_PATTERN.test(raw)) return false;
6623
+ if (WORKFLOW_DOWNLOAD_ARTIFACT_PATTERN.test(raw)) return false;
6624
+ const workingDirectory = WORKFLOW_WORKING_DIRECTORY_PATTERN.exec(raw);
6625
+ if (workingDirectory) {
6626
+ const value = beforeShellComment(workingDirectory[1] ?? "").trim().replace(/^(['"])(.*)\1$/, "$2");
6627
+ if (!isCheckoutDirectory(inventory, value)) return false;
6628
+ }
6629
+ }
6630
+ return true;
6631
+ }
6632
+ function findLockfileRequirement(repoPath, workflows, inventory, isSatisfied) {
6633
+ for (const file of workflows) {
6634
+ const fullPath = join(repoPath, file);
6635
+ if (!isRegularFile(fullPath)) continue;
6636
+ const contents = readRepoText(fullPath, "utf8");
6637
+ if (!workflowReadsOnlyThisTree(inventory, contents)) continue;
6638
+ const steps = githubActionsRunSteps(contents);
6639
+ const commandsOf = (step) => step.lines.flatMap(({ line, text }) => shellCommands(text).map((words) => ({ line, words })));
6640
+ const stepCommands = steps.map(commandsOf);
6641
+ const mayBeGenerated = /* @__PURE__ */ new Set();
6642
+ for (const { words } of stepCommands.flat()) {
6643
+ for (const rule of LOCKFILE_REQUIRING_COMMANDS) {
6644
+ if (mayBeGenerated.has(rule)) continue;
6645
+ const generates = words[0] === rule.tool && rule.generators.includes(words[1] ?? "") && matchLockfileRequiringCommand(words, rule) === void 0;
6646
+ const names = words.some((word) => rule.lockfiles.some((lockfile) => word.includes(lockfile)));
6647
+ if (generates || names) mayBeGenerated.add(rule);
6648
+ }
6649
+ }
6650
+ for (const [stepIndex, step] of steps.entries()) {
6651
+ if (step.conditional) continue;
6652
+ const commands = stepCommands[stepIndex] ?? [];
6653
+ const shellIsConditional = step.lines.some(({ text }) => beforeShellComment(text).includes("<<")) || commands.some(({ words }) => isConditionalShellCommand(words)) || step.lines.some(({ text }) => /(^|[ \t])\|\|([ \t]|$)/.test(beforeShellComment(text)));
6654
+ if (shellIsConditional) continue;
6655
+ const leavesCheckout = commands.some(({ words }) => {
6656
+ const head = commandHead(words);
6657
+ return DIRECTORY_CHANGE_COMMANDS.has(head.word) && !isCheckoutDirectory(inventory, words[head.argsFrom] ?? "");
6658
+ });
6659
+ if (leavesCheckout) continue;
6660
+ for (const { line, words } of commands) {
6661
+ for (const rule of LOCKFILE_REQUIRING_COMMANDS) {
6662
+ const command = matchLockfileRequiringCommand(words, rule);
6663
+ if (command === void 0 || isSatisfied(rule.lockfiles) || mayBeGenerated.has(rule) || !commandStaysInCheckout(words, rule, inventory)) {
6664
+ continue;
6665
+ }
6666
+ return { file, line, command: command.slice(0, 80), lockfiles: rule.lockfiles };
6667
+ }
6668
+ }
6669
+ }
6670
+ }
6671
+ return void 0;
5405
6672
  }
5406
6673
  var LIBRARY_DEPENDENCY_COUNT_SANE_MAX = 120;
5407
6674
  function libraryDependencyCountSanity(specCount) {
@@ -5606,7 +6873,20 @@ function collectA5ClaimRealityEvidence(repoPath, repoFiles, useV27Detectors, use
5606
6873
  ...reconciliationUnsettled ? { notes: WITAN_A5_RECONCILIATION_UNSETTLED_DISCLOSURE } : {}
5607
6874
  };
5608
6875
  }
5609
- function collectB2PrTraceEvidence(repoPath, repoFiles) {
6876
+ var RECENT_COMMIT_WINDOW = 12;
6877
+ var V27_A2_SHALLOW_HISTORY_NOTE = "A2 abstained: this repository is a shallow clone, so the scan for credentials and .env files in earlier commits read only the commits present, and no confirmed critical finding stands on what was read. Re-scan a full clone (for actions/checkout, set fetch-depth: 0).";
6878
+ var V27_B2_SHALLOW_HISTORY_NOTE = `B2 abstained: this repository is a shallow clone with fewer than ${RECENT_COMMIT_WINDOW} commits visible, so the pull-request merge ratio cannot be read over its window. Re-scan a full clone (for actions/checkout, set fetch-depth: 0).`;
6879
+ function shallowHistoryAbstention(criterionId, notes) {
6880
+ return {
6881
+ criterionId,
6882
+ positiveEvidence: [],
6883
+ findings: [],
6884
+ metrics: [],
6885
+ insufficientData: true,
6886
+ notes
6887
+ };
6888
+ }
6889
+ function collectB2PrTraceEvidence(repoPath, repoFiles, useV27FalseAssuranceClosure = false) {
5610
6890
  const workflows = repoFiles.filter(isCiWorkflow);
5611
6891
  const prTemplate = repoFiles.find(
5612
6892
  (file) => /(^|\/)(pull_request_template|PULL_REQUEST_TEMPLATE)(\.md$|\/[^/]+\.md$)/.test(file)
@@ -5615,15 +6895,21 @@ function collectB2PrTraceEvidence(repoPath, repoFiles) {
5615
6895
  (file) => /branch.*protection|review.*gate/i.test(file) || GITHUB_CODEOWNERS_PATH_PATTERN.test(file)
5616
6896
  );
5617
6897
  const citedWorkflows = workflows.slice(0, 3);
5618
- const workflowLabel = ciWorkflowCitationLabel(
5619
- "Pull-request CI workflow",
5620
- "counted",
5621
- workflows.length,
5622
- citedWorkflows.length,
5623
- unreadCiWorkflowReasons(() => true)
6898
+ const unreadWorkflows = unreadCiWorkflowReasons(() => true);
6899
+ const workflowLabel = (file) => fittedCiWorkflowCitationLabel(
6900
+ b2WorkflowCitationBaseLabels(repoPath, file),
6901
+ (baseLabel) => ciWorkflowCitationLabel(
6902
+ baseLabel,
6903
+ "counted",
6904
+ workflows.length,
6905
+ citedWorkflows.length,
6906
+ unreadWorkflows
6907
+ )
5624
6908
  );
5625
6909
  const evidence = [
5626
- ...citedWorkflows.map((file) => evidenceForRelative(repoPath, file, "ci_run", workflowLabel)),
6910
+ ...citedWorkflows.map(
6911
+ (file) => evidenceForRelative(repoPath, file, "ci_run", workflowLabel(file))
6912
+ ),
5627
6913
  ...prTemplate ? [evidenceForRelative(repoPath, prTemplate, "pull_request", "Pull request template")] : [],
5628
6914
  ...branchProtectionDoc ? [
5629
6915
  evidenceForRelative(
@@ -5635,6 +6921,9 @@ function collectB2PrTraceEvidence(repoPath, repoFiles) {
5635
6921
  ] : []
5636
6922
  ];
5637
6923
  if (evidence.length === 0) return null;
6924
+ if (useV27FalseAssuranceClosure && isShallowRepository(repoPath) && readRecentCommits(repoPath).length < RECENT_COMMIT_WINDOW) {
6925
+ return shallowHistoryAbstention("B2", V27_B2_SHALLOW_HISTORY_NOTE);
6926
+ }
5638
6927
  return {
5639
6928
  criterionId: "B2",
5640
6929
  positiveEvidence: evidence,
@@ -5678,18 +6967,46 @@ function collectB2PrTraceEvidence(repoPath, repoFiles) {
5678
6967
  ]
5679
6968
  };
5680
6969
  }
5681
- function collectB3CiDisciplineEvidence(repoPath, repoFiles, mono) {
6970
+ function collectB3CiDisciplineEvidence(repoPath, repoFiles, mono, useV27FalseAssuranceClosure = false) {
5682
6971
  const evidence = [];
5683
6972
  const packageJson = findRootPackageJson(repoFiles);
5684
6973
  const scripts = packageJson ? readPackageScripts(join(repoPath, packageJson)) : /* @__PURE__ */ new Map();
5685
6974
  const workflows = repoFiles.filter(isCiWorkflow);
5686
6975
  const workflow = workflows[0];
5687
6976
  const rootWorkflows = mono ? mono.sharedFiles.filter(isCiWorkflow) : [];
5688
- const defaultBranchCiCount = withContentReadSignal(
6977
+ const knownPaths = [...repoFiles, ...withheldRepoPaths().map(({ path }) => path)];
6978
+ const ownInventory = {
6979
+ scanned: new Set(repoFiles),
6980
+ known: new Set(knownPaths),
6981
+ hasYarnPlugins: knownPaths.some((path) => path.startsWith(".yarn/plugins/"))
6982
+ };
6983
+ const countsAsCi = (root) => (file) => !useV27FalseAssuranceClosure || !isPublishOnlyWorkflow(root, file, root === repoPath ? ownInventory : null);
6984
+ const { defaultBranchCiCount, defaultBranchCiWorkflows, rootDefaultBranchCiWorkflows } = withContentReadSignal(
5689
6985
  "B3",
5690
6986
  "default_branch_ci_depth",
5691
- () => workflows.filter((file) => workflowTargetsDefaultBranch(repoPath, file)).length + (mono ? rootWorkflows.filter((file) => workflowTargetsDefaultBranch(mono.root, file)).length : 0)
6987
+ () => {
6988
+ const counted = workflows.filter(countsAsCi(repoPath)).filter((file) => workflowTargetsDefaultBranch(repoPath, file));
6989
+ const rootCounted = mono ? rootWorkflows.filter(countsAsCi(mono.root)).filter((file) => workflowTargetsDefaultBranch(mono.root, file)) : [];
6990
+ return {
6991
+ defaultBranchCiCount: counted.length + rootCounted.length,
6992
+ defaultBranchCiWorkflows: counted,
6993
+ rootDefaultBranchCiWorkflows: rootCounted
6994
+ };
6995
+ }
6996
+ );
6997
+ const unshownCheckWorkflows = mergeB3UnshownCheckWorkflows(
6998
+ findB3UnshownCheckWorkflows(repoPath, defaultBranchCiWorkflows),
6999
+ mono ? findB3UnshownCheckWorkflows(
7000
+ mono.root,
7001
+ rootDefaultBranchCiWorkflows,
7002
+ (file) => `${file} at the monorepo root`
7003
+ ) : { printOnly: [] }
5692
7004
  );
7005
+ const publishOnlyWorkflows = useV27FalseAssuranceClosure ? withContentReadSignals(
7006
+ "B3",
7007
+ ["ci_script_depth", "default_branch_ci_depth"],
7008
+ () => workflows.filter((file) => isPublishOnlyWorkflow(repoPath, file, ownInventory))
7009
+ ) : [];
5693
7010
  if (packageJson && scripts.has("test")) {
5694
7011
  evidence.push(evidenceForRelative(repoPath, packageJson, "test_run", "Test script"));
5695
7012
  }
@@ -5771,10 +7088,10 @@ function collectB3CiDisciplineEvidence(repoPath, repoFiles, mono) {
5771
7088
  "ci_script_depth",
5772
7089
  () => ({
5773
7090
  repoCategoryHits: CI_COMMAND_CATEGORIES.map(
5774
- (pattern) => workflows.some((file) => fileContains(repoPath, file, pattern))
7091
+ (pattern) => workflows.filter(countsAsCi(repoPath)).some((file) => fileContains(repoPath, file, pattern))
5775
7092
  ),
5776
7093
  rootCategoryHits: mono ? CI_COMMAND_CATEGORIES.map(
5777
- (pattern) => rootWorkflows.some((file) => fileContains(mono.root, file, pattern))
7094
+ (pattern) => rootWorkflows.filter(countsAsCi(mono.root)).some((file) => fileContains(mono.root, file, pattern))
5778
7095
  ) : []
5779
7096
  })
5780
7097
  );
@@ -5820,10 +7137,655 @@ function collectB3CiDisciplineEvidence(repoPath, repoFiles, mono) {
5820
7137
  0.55,
5821
7138
  "workflows",
5822
7139
  "Counts CI workflows that target pull requests or the default branch, up to 4.",
5823
- "saturating_count"
7140
+ "saturating_count",
7141
+ b3UnshownCheckCount(unshownCheckWorkflows) > 0 ? { condition: b3UnshownCheckCondition(unshownCheckWorkflows) } : void 0
5824
7142
  )
5825
- ]
7143
+ ],
7144
+ ...publishOnlyWorkflows.length > 0 ? { notes: b3PublishOnlyNote(publishOnlyWorkflows) } : {},
7145
+ ...b3UnshownCheckDisclosures(unshownCheckWorkflows)
7146
+ };
7147
+ }
7148
+ function b3UnshownCheckDisclosures(unshown) {
7149
+ const sentence = b3UnshownCheckDisclosure(unshown, CRITERION_DISCLOSURE_MAX_LENGTH);
7150
+ return sentence ? { disclosures: [sentence] } : {};
7151
+ }
7152
+ function b3PublishOnlyNote(publishOnlyWorkflows) {
7153
+ return `${publishOnlyWorkflows.length === 1 ? "1 workflow that only publishes, releases or deploys" : `${publishOnlyWorkflows.length} workflows that only publish, release or deploy`} (no pull-request trigger, and every step Cejel reads only checks out, sets up a toolchain, configures credentials or uploads an already-built artifact) ${publishOnlyWorkflows.length === 1 ? "is" : "are"} not counted as CI verification: ${publishOnlyWorkflows.slice(0, 4).join(", ")}${publishOnlyWorkflows.length > 4 ? ", ..." : ""}.`.slice(0, CRITERION_NOTES_MAX_LENGTH);
7154
+ }
7155
+ var PUBLISH_OR_DEPLOY_STEP_PATTERN = /\b(?:npm|pnpm|yarn|bun)[ \t]+(?:npm[ \t]+)?publish\b|\bpypa\/gh-action-pypi-publish\b|\btwine[ \t]+upload\b|\b(?:uv|poetry|flit|hatch|cargo)[ \t]+publish\b|\bgem[ \t]+push\b|\bgh[ \t]+release[ \t]+(?:create|upload)\b|\bsoftprops\/action-gh-release\b|\bgoreleaser\b|\bsemantic-release\b|\bchangesets\/action\b|\bdocker[ \t]+(?:image[ \t]+)?push\b|\bdocker\/build-push-action\b|\b(?:vercel|netlify|flyctl|fly|firebase|serverless|wrangler)[ \t]+deploy\b|\bkubectl[ \t]+apply\b|\bhelm[ \t]+upgrade\b|\bterraform[ \t]+apply\b/i;
7156
+ var PUBLISH_ONLY_EXTRA_VERIFICATION_PATTERN = /\b(?:tox|nox|pre-commit|bazel[ \t]+test|gradlew?[ \t]+(?:test|check|build)|mvnw?[ \t]+(?:verify|test|package)|make[ \t]+(?:check|test)|just[ \t]+test|cargo[ \t]+(?:clippy|check|nextest)|turbo[ \t]+(?:run[ \t]+)?test)\b|[ \t]-r[ \t]+test\b|\brun[ \t]+test\b/i;
7157
+ var PULL_REQUEST_TRIGGERS = ["pull_request", "pull_request_target", "merge_group"];
7158
+ var PULL_REQUEST_TRIGGER_PATTERN = /\b(?:pull_request|pull_request_target|merge_group)\b/;
7159
+ var REUSABLE_WORKFLOW_CALL_PATTERN = /^[ \t]*uses:[ \t]*\S*\.github\/workflows\//m;
7160
+ function isPublishOnlyWorkflow(repoPath, file, inventory) {
7161
+ if (!isGitHubActionsWorkflow(file)) return false;
7162
+ const fullPath = join(repoPath, file);
7163
+ if (!isRegularFile(fullPath)) return false;
7164
+ const contents = readRepoText(fullPath, "utf8");
7165
+ if (!PUBLISH_OR_DEPLOY_STEP_PATTERN.test(contents) || PULL_REQUEST_TRIGGER_PATTERN.test(contents) || REUSABLE_WORKFLOW_CALL_PATTERN.test(contents) || CI_TEST_COMMAND_PATTERN.test(contents) || CI_LINT_COMMAND_PATTERN.test(contents) || CI_TYPECHECK_COMMAND_PATTERN.test(contents) || PUBLISH_ONLY_EXTRA_VERIFICATION_PATTERN.test(contents) || YAML_LINE_BREAK_NOT_SPLIT_PATTERN.test(contents)) {
7166
+ return false;
7167
+ }
7168
+ const steps = placedPublishWorkflowSteps(contents);
7169
+ if (steps === null) return false;
7170
+ let publishes = false;
7171
+ const needs = /* @__PURE__ */ new Set();
7172
+ for (const step of steps) {
7173
+ const classified = step.uses !== void 0 ? { kind: classifyPublishOnlyUsesStep(step.uses, step.inputs) } : classifyPublishOnlyRunLines(step.run ?? []);
7174
+ if (classified.kind === "other") return false;
7175
+ if (classified.kind === "publish") publishes = true;
7176
+ for (const need of classified.needs ?? []) needs.add(need);
7177
+ }
7178
+ if (!publishes) return false;
7179
+ if (needs.size > 0 && inventory === null) return false;
7180
+ if (needs.has("package") && !packageManifestRunsNoRepositoryCode(repoPath, inventory)) {
7181
+ return false;
7182
+ }
7183
+ return true;
7184
+ }
7185
+ function classifyPublishOnlyRunLines(lines) {
7186
+ let kind = "setup";
7187
+ const needs = /* @__PURE__ */ new Set();
7188
+ for (const text of joinShellContinuationLines(lines)) {
7189
+ for (const words of shellCommands(text)) {
7190
+ const command = classifyPublishOnlyRunCommand(words);
7191
+ if (command.kind === "other") return { kind: "other" };
7192
+ if (command.kind === "publish") kind = "publish";
7193
+ for (const need of command.needs ?? []) needs.add(need);
7194
+ }
7195
+ }
7196
+ return { kind, needs: [...needs] };
7197
+ }
7198
+ var PACKAGE_LIFECYCLE_SCRIPTS = [
7199
+ "preinstall",
7200
+ "install",
7201
+ "postinstall",
7202
+ "preprepare",
7203
+ "prepare",
7204
+ "postprepare",
7205
+ "prepack",
7206
+ "postpack",
7207
+ "prepublish",
7208
+ "prepublishOnly",
7209
+ "publish",
7210
+ "postpublish",
7211
+ "dependencies"
7212
+ ];
7213
+ var PACKAGE_MANAGER_CODE_CONFIG_FILES = [
7214
+ ".npmrc",
7215
+ ".yarnrc",
7216
+ ".yarnrc.yml",
7217
+ ".pnpmfile.cjs",
7218
+ "pnpmfile.js",
7219
+ "bunfig.toml",
7220
+ "pnpm-workspace.yaml",
7221
+ "lerna.json",
7222
+ "binding.gyp"
7223
+ ];
7224
+ var PACKAGE_DEPENDENCY_FIELDS = ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"];
7225
+ function packageManifestRunsNoRepositoryCode(root, inventory) {
7226
+ if (!inventory.scanned.has("package.json")) return false;
7227
+ if (PACKAGE_MANAGER_CODE_CONFIG_FILES.some((file) => inventory.known.has(file))) return false;
7228
+ if (inventory.hasYarnPlugins) return false;
7229
+ const manifest = parseJsonObject(readRepoText(join(root, "package.json"), "utf8"));
7230
+ if (!manifest || "workspaces" in manifest || "gypfile" in manifest) return false;
7231
+ const scripts = manifest.scripts;
7232
+ if (scripts !== void 0 && !isRecord(scripts)) return false;
7233
+ if (isRecord(scripts) && PACKAGE_LIFECYCLE_SCRIPTS.some((name) => name in scripts)) return false;
7234
+ for (const field of PACKAGE_DEPENDENCY_FIELDS) {
7235
+ const dependencies = manifest[field];
7236
+ if (dependencies === void 0) continue;
7237
+ if (!isRecord(dependencies)) return false;
7238
+ for (const spec of Object.values(dependencies)) {
7239
+ if (typeof spec !== "string") return false;
7240
+ if (/^(?:file|link|workspace|portal):|^\.{0,2}\//.test(spec.trim())) return false;
7241
+ }
7242
+ }
7243
+ return true;
7244
+ }
7245
+ var PUBLISH_ONLY_JOB_KEYS = /* @__PURE__ */ new Set(["runs-on", "name", "timeout-minutes", "needs", "permissions", "environment", "steps"]);
7246
+ var PUBLISH_ONLY_JOB_MAPPING_KEYS = /* @__PURE__ */ new Set(["permissions", "environment"]);
7247
+ var PUBLISH_ONLY_STEP_KEYS = /* @__PURE__ */ new Set(["name", "id", "uses", "run", "with", "env"]);
7248
+ var PLAIN_MAPPING_ENTRY_PATTERN = /^([A-Za-z_][A-Za-z0-9_-]*):(?:[ \t]+([^]*))?$/;
7249
+ var NEEDS_FLOW_SEQUENCE_PATTERN = /^\[[ \t]*[A-Za-z0-9_-]+(?:[ \t]*,[ \t]*[A-Za-z0-9_-]+)*[ \t]*\]$/;
7250
+ function isPlainOneLineValue(value) {
7251
+ return value !== "" && !/^[[{&*|>!]/.test(value) && isOneLineScalar(value);
7252
+ }
7253
+ function placedPublishWorkflowSteps(contents) {
7254
+ if (YAML_LINE_BREAK_NOT_SPLIT_PATTERN.test(contents) || OTHER_LEADING_BLANK_PATTERN.test(contents)) return null;
7255
+ const lines = contents.split(/\r?\n/);
7256
+ const isContent = (line) => line.trim() !== "" && !line.trimStart().startsWith("#");
7257
+ let jobsStart = -1;
7258
+ for (let index = 0; index < lines.length; index += 1) {
7259
+ const raw = lines[index] ?? "";
7260
+ if (!isContent(raw) || /^[ \t]/.test(raw)) continue;
7261
+ if (!PRINT_ONLY_TOP_LEVEL_KEY_PATTERN.test(raw)) return null;
7262
+ if (/^jobs:[ \t]*$/.test(stripYamlComment(raw))) {
7263
+ if (jobsStart !== -1) return null;
7264
+ jobsStart = index;
7265
+ }
7266
+ }
7267
+ if (jobsStart === -1) return null;
7268
+ const steps = [];
7269
+ let jobIndent = -1;
7270
+ let keyIndent = -1;
7271
+ let inSteps = false;
7272
+ let stepKeyColumn = -1;
7273
+ let step = null;
7274
+ let body = null;
7275
+ const openStep = () => step;
7276
+ const closeStep = () => {
7277
+ if (step === null) return true;
7278
+ const placed = step;
7279
+ step = null;
7280
+ if (placed.uses === void 0 === (placed.run === void 0)) return false;
7281
+ steps.push(placed);
7282
+ return true;
7283
+ };
7284
+ for (let index = jobsStart + 1; index < lines.length; index += 1) {
7285
+ const raw = lines[index] ?? "";
7286
+ if (raw.trim() === "") continue;
7287
+ const indent = lineIndent(raw);
7288
+ if (body) {
7289
+ if (indent > body.column) {
7290
+ if (body.kind === "plain") return null;
7291
+ if (body.kind === "run") {
7292
+ openStep()?.run?.push(raw.trim());
7293
+ continue;
7294
+ }
7295
+ if (body.textColumn !== -1 && indent > body.textColumn) continue;
7296
+ body.textColumn = -1;
7297
+ if (!isContent(raw)) continue;
7298
+ if (body.entryColumn === -1) body.entryColumn = indent;
7299
+ if (indent !== body.entryColumn) return null;
7300
+ const entry = PLAIN_MAPPING_ENTRY_PATTERN.exec(stripYamlComment(raw).trim());
7301
+ if (!entry) return null;
7302
+ const name = entry[1] ?? "";
7303
+ const value2 = (entry[2] ?? "").trim();
7304
+ if (body.owner === "env" && isCodeLoadingEnvKey(name)) return null;
7305
+ if (body.owner === "with") {
7306
+ openStep()?.inputs.add(name);
7307
+ if (BLOCK_SCALAR_INDICATOR_PATTERN.test(value2)) {
7308
+ body.textColumn = indent;
7309
+ continue;
7310
+ }
7311
+ }
7312
+ if (!isPlainOneLineValue(value2)) return null;
7313
+ continue;
7314
+ }
7315
+ body = null;
7316
+ }
7317
+ if (!isContent(raw)) continue;
7318
+ const text = stripYamlComment(raw).trimEnd();
7319
+ if (indent === 0) break;
7320
+ if (jobIndent === -1) jobIndent = indent;
7321
+ if (indent < jobIndent) return null;
7322
+ if (indent === jobIndent) {
7323
+ if (!closeStep()) return null;
7324
+ if (!/^[ \t]*[A-Za-z_][A-Za-z0-9_-]*:[ \t]*$/.test(text)) return null;
7325
+ keyIndent = -1;
7326
+ inSteps = false;
7327
+ continue;
7328
+ }
7329
+ if (keyIndent === -1) keyIndent = indent;
7330
+ const isItem = /^[ \t]*-[ \t]+/.test(text);
7331
+ if (indent < keyIndent) return null;
7332
+ if (indent === keyIndent && !(inSteps && isItem)) {
7333
+ if (!closeStep()) return null;
7334
+ inSteps = false;
7335
+ const key2 = UNQUOTED_KEY_PATTERN.exec(text.trim());
7336
+ if (!key2 || !PUBLISH_ONLY_JOB_KEYS.has(key2[1] ?? "")) return null;
7337
+ const value2 = (key2[2] ?? "").trim();
7338
+ if (key2[1] === "steps") {
7339
+ if (value2 !== "") return null;
7340
+ inSteps = true;
7341
+ continue;
7342
+ }
7343
+ if (value2 === "" && PUBLISH_ONLY_JOB_MAPPING_KEYS.has(key2[1] ?? "")) {
7344
+ body = { kind: "mapping", column: indent, entryColumn: -1, textColumn: -1, owner: "job" };
7345
+ continue;
7346
+ }
7347
+ if (key2[1] === "needs" && NEEDS_FLOW_SEQUENCE_PATTERN.test(value2)) continue;
7348
+ if (!isPlainOneLineValue(value2)) return null;
7349
+ body = { kind: "plain", column: indent };
7350
+ continue;
7351
+ }
7352
+ if (!inSteps) return null;
7353
+ let keyText = text;
7354
+ let column = indent;
7355
+ if (isItem) {
7356
+ if (!closeStep()) return null;
7357
+ const item = /^([ \t]*-[ \t]+)([^]*)$/.exec(text);
7358
+ keyText = item?.[2] ?? "";
7359
+ column = (item?.[1] ?? "").length;
7360
+ stepKeyColumn = column;
7361
+ step = { inputs: /* @__PURE__ */ new Set() };
7362
+ } else if (indent !== stepKeyColumn || !step) {
7363
+ return null;
7364
+ }
7365
+ const key = UNQUOTED_KEY_PATTERN.exec(keyText.trim());
7366
+ if (!key || !PUBLISH_ONLY_STEP_KEYS.has(key[1] ?? "")) return null;
7367
+ const value = (key[2] ?? "").trim();
7368
+ const current = step ?? { inputs: /* @__PURE__ */ new Set() };
7369
+ if (key[1] === "with" || key[1] === "env") {
7370
+ if (value !== "") return null;
7371
+ body = { kind: "mapping", column, entryColumn: -1, textColumn: -1, owner: key[1] };
7372
+ } else if (key[1] === "uses") {
7373
+ if (current.uses !== void 0 || !isPlainOneLineValue(value)) return null;
7374
+ current.uses = value.replace(/^(['"])(.*)\1$/, "$2");
7375
+ body = { kind: "plain", column };
7376
+ } else if (key[1] === "run") {
7377
+ if (current.run !== void 0) return null;
7378
+ if (BLOCK_SCALAR_INDICATOR_PATTERN.test(value)) {
7379
+ current.run = [];
7380
+ body = { kind: "run", column };
7381
+ } else {
7382
+ if (!isPlainOneLineValue(value) || /^["']/.test(value)) return null;
7383
+ current.run = [value];
7384
+ body = { kind: "plain", column };
7385
+ }
7386
+ } else {
7387
+ if (!isPlainOneLineValue(value)) return null;
7388
+ body = { kind: "plain", column };
7389
+ }
7390
+ }
7391
+ if (!closeStep()) return null;
7392
+ return steps;
7393
+ }
7394
+ function joinShellContinuationLines(lines) {
7395
+ const joined = [];
7396
+ let pending = "";
7397
+ for (const line of lines) {
7398
+ const code = beforeShellComment(line).trimEnd();
7399
+ if (code.endsWith("\\")) {
7400
+ pending += `${code.slice(0, -1)} `;
7401
+ continue;
7402
+ }
7403
+ joined.push(`${pending}${line}`);
7404
+ pending = "";
7405
+ }
7406
+ if (pending !== "") joined.push(pending);
7407
+ return joined;
7408
+ }
7409
+ var CODE_LOADING_ENV_KEYS = /* @__PURE__ */ new Set([
7410
+ "BASH_ENV",
7411
+ "ENV",
7412
+ "NODE_OPTIONS",
7413
+ "LD_PRELOAD",
7414
+ "LD_AUDIT",
7415
+ "LD_LIBRARY_PATH",
7416
+ "PYTHONPATH",
7417
+ "PYTHONSTARTUP",
7418
+ "PERL5OPT",
7419
+ "PERL5LIB",
7420
+ "RUBYOPT",
7421
+ "PATH",
7422
+ "NPM_CONFIG_NODE_OPTIONS",
7423
+ "NPM_CONFIG_SCRIPT_SHELL"
7424
+ ]);
7425
+ function isCodeLoadingEnvKey(name) {
7426
+ return CODE_LOADING_ENV_KEYS.has(name) || CODE_LOADING_ENV_KEYS.has(name.toUpperCase());
7427
+ }
7428
+ var PACKAGE_TARGET_FLAGS = /* @__PURE__ */ new Set([
7429
+ "--prefix",
7430
+ "-C",
7431
+ "--dir",
7432
+ "--cwd",
7433
+ "--workspace",
7434
+ "-w",
7435
+ "--workspaces",
7436
+ "-ws",
7437
+ "--filter",
7438
+ "-F",
7439
+ "-r",
7440
+ "--recursive",
7441
+ "--global",
7442
+ "-g"
7443
+ ]);
7444
+ function flagName(word) {
7445
+ const equals = word.indexOf("=");
7446
+ return equals === -1 ? word : word.slice(0, equals);
7447
+ }
7448
+ function targetsRootPackageOnly(args) {
7449
+ return args.every((word) => word.startsWith("-") && !PACKAGE_TARGET_FLAGS.has(flagName(word)));
7450
+ }
7451
+ function isRegistryConfigKey(key) {
7452
+ return key.startsWith("//") || key === "registry" || key.endsWith(":registry");
7453
+ }
7454
+ function classifyPublishOnlyRunCommand(input) {
7455
+ const other = { kind: "other" };
7456
+ if (input.some((word) => /[`&|;<>]|\$\(/.test(word))) return other;
7457
+ let start = 0;
7458
+ while (start < input.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(input[start] ?? "")) {
7459
+ if (isCodeLoadingEnvKey(flagName(input[start] ?? ""))) return other;
7460
+ start += 1;
7461
+ }
7462
+ if (start === input.length) return other;
7463
+ const words = input.slice(start);
7464
+ const [first = "", second = "", third = ""] = words;
7465
+ if (words.some((word) => word.startsWith("--dry-run"))) return other;
7466
+ switch (first) {
7467
+ case "npm":
7468
+ case "pnpm":
7469
+ case "yarn": {
7470
+ if (second === "config") {
7471
+ return third === "set" && isRegistryConfigKey(words[3] ?? "") && words.length <= 5 ? { kind: "setup" } : other;
7472
+ }
7473
+ const publishFrom = second === "publish" ? 2 : first === "yarn" && second === "npm" && third === "publish" ? 3 : -1;
7474
+ if (publishFrom === -1) return other;
7475
+ return targetsRootPackageOnly(words.slice(publishFrom)) ? { kind: "publish", needs: ["package"] } : other;
7476
+ }
7477
+ case "twine":
7478
+ return second === "upload" ? { kind: "publish" } : other;
7479
+ case "uv":
7480
+ return second === "publish" && !words.some((word) => word.includes("build")) ? { kind: "publish" } : other;
7481
+ case "poetry":
7482
+ return second === "publish" && !words.some((word) => word.startsWith("--build")) ? { kind: "publish" } : other;
7483
+ case "gh":
7484
+ return second === "release" && (third === "create" || third === "upload") ? { kind: "publish" } : other;
7485
+ case "cargo":
7486
+ return second === "publish" && words.includes("--no-verify") && !words.includes("-n") ? { kind: "publish" } : other;
7487
+ default:
7488
+ return other;
7489
+ }
7490
+ }
7491
+ var PUBLISH_ONLY_SETUP_ACTIONS = /* @__PURE__ */ new Map([
7492
+ ["actions/checkout", ["fetch-depth", "token", "persist-credentials", "fetch-tags", "show-progress"]],
7493
+ ["actions/cache", null],
7494
+ ["actions/upload-artifact", null],
7495
+ ["actions/download-artifact", ["name", "path", "pattern", "merge-multiple", "run-id", "github-token"]],
7496
+ ["actions/setup-node", null],
7497
+ ["actions/setup-python", null],
7498
+ ["actions/setup-go", null],
7499
+ ["actions/setup-java", null],
7500
+ ["actions/setup-dotnet", null],
7501
+ ["astral-sh/setup-uv", null],
7502
+ ["oven-sh/setup-bun", null],
7503
+ ["pnpm/action-setup", ["version", "package_json_file", "dest", "standalone"]],
7504
+ ["docker/login-action", null]
7505
+ ]);
7506
+ function classifyPublishOnlyUsesStep(uses, inputs) {
7507
+ const at = uses.indexOf("@");
7508
+ if (at === -1) return "other";
7509
+ const action = uses.slice(0, at);
7510
+ if (action === "pypa/gh-action-pypi-publish") return "publish";
7511
+ if (action === "softprops/action-gh-release") return inputs.has("files") ? "publish" : "other";
7512
+ if (!PUBLISH_ONLY_SETUP_ACTIONS.has(action)) return "other";
7513
+ const allowed = PUBLISH_ONLY_SETUP_ACTIONS.get(action);
7514
+ return allowed === null || [...inputs].every((input) => (allowed ?? []).includes(input)) ? "setup" : "other";
7515
+ }
7516
+ var WORKFLOW_ON_KEY_PATTERN = /^(?:on|"on"|'on'|true):(?:[ \t]|$)/;
7517
+ var WORKFLOW_TRIGGER_NAME_PATTERN = /^[a-z_]+$/;
7518
+ var YAML_LINE_BREAK_NOT_SPLIT_PATTERN = /\r(?!\n)|[\u0085\u2028\u2029]/;
7519
+ function stripYamlComment(text) {
7520
+ return text.replace(/(?:^|[ \t])#[^]*$/, "");
7521
+ }
7522
+ function unquoteYamlScalar(text) {
7523
+ return text.trim().replace(/^(['"])(.*)\1$/, "$2");
7524
+ }
7525
+ function yamlInlineList(text) {
7526
+ const value = stripYamlComment(text).trim();
7527
+ if (value === "") return [];
7528
+ const flow = /^\[(.*)\]$/.exec(value);
7529
+ if (flow) {
7530
+ return (flow[1] ?? "").split(",").map(unquoteYamlScalar).filter((item) => item !== "");
7531
+ }
7532
+ return /^[{&*|>]/.test(value) ? null : [unquoteYamlScalar(value)];
7533
+ }
7534
+ function githubWorkflowTriggers(contents) {
7535
+ if (YAML_LINE_BREAK_NOT_SPLIT_PATTERN.test(contents)) return null;
7536
+ const lines = contents.split(/\r?\n/);
7537
+ const onIndex = lines.findIndex((line) => WORKFLOW_ON_KEY_PATTERN.test(line));
7538
+ if (onIndex === -1) return null;
7539
+ const onLine = lines[onIndex] ?? "";
7540
+ const inline = yamlInlineList(onLine.slice(onLine.indexOf(":") + 1));
7541
+ if (inline === null) return null;
7542
+ const triggers = /* @__PURE__ */ new Set();
7543
+ if (inline.length > 0) {
7544
+ for (const name of inline) {
7545
+ if (!WORKFLOW_TRIGGER_NAME_PATTERN.test(name)) return null;
7546
+ triggers.add(name);
7547
+ }
7548
+ return triggers;
7549
+ }
7550
+ const isContent = (line) => line.trim() !== "" && !line.trimStart().startsWith("#");
7551
+ const firstIndex = lines.findIndex((line, index) => index > onIndex && isContent(line));
7552
+ if (firstIndex !== -1 && /^-[ \t]/.test(lines[firstIndex] ?? "")) {
7553
+ for (let index = firstIndex; index < lines.length; index += 1) {
7554
+ const raw = lines[index] ?? "";
7555
+ if (!isContent(raw)) continue;
7556
+ const item = /^-[ \t]+([^]*)$/.exec(raw);
7557
+ if (!item) break;
7558
+ const name = unquoteYamlScalar(stripYamlComment(item[1] ?? ""));
7559
+ if (!WORKFLOW_TRIGGER_NAME_PATTERN.test(name)) return null;
7560
+ triggers.add(name);
7561
+ }
7562
+ return triggers.size > 0 ? triggers : null;
7563
+ }
7564
+ let triggerIndent = -1;
7565
+ let mappingForm = null;
7566
+ let inTrigger = false;
7567
+ for (let index = onIndex + 1; index < lines.length; index += 1) {
7568
+ const raw = lines[index] ?? "";
7569
+ if (!isContent(raw)) continue;
7570
+ const indent = lineIndent(raw);
7571
+ if (indent === 0) break;
7572
+ if (triggerIndent === -1) triggerIndent = indent;
7573
+ if (indent < triggerIndent) return null;
7574
+ if (indent > triggerIndent) {
7575
+ if (!inTrigger) return null;
7576
+ continue;
7577
+ }
7578
+ const item = /^[ \t]*-[ \t]+([^]*)$/.exec(raw);
7579
+ if (item && mappingForm === true) continue;
7580
+ if (item) {
7581
+ mappingForm = false;
7582
+ inTrigger = false;
7583
+ const name = unquoteYamlScalar(stripYamlComment(item[1] ?? ""));
7584
+ if (!WORKFLOW_TRIGGER_NAME_PATTERN.test(name)) return null;
7585
+ triggers.add(name);
7586
+ continue;
7587
+ }
7588
+ if (mappingForm === false) return null;
7589
+ mappingForm = true;
7590
+ const key = /^[ \t]*([A-Za-z_]+):([^]*)$/.exec(raw);
7591
+ if (!key || !WORKFLOW_TRIGGER_NAME_PATTERN.test(key[1] ?? "")) return null;
7592
+ const rest = stripYamlComment(key[2] ?? "").trim();
7593
+ if (rest !== "" && rest !== "{}" && rest !== "null" && rest !== "~") return null;
7594
+ triggers.add(key[1] ?? "");
7595
+ inTrigger = true;
7596
+ }
7597
+ return triggers.size > 0 ? triggers : null;
7598
+ }
7599
+ var PULL_REQUEST_TRIGGER_NAMES = new Set(PULL_REQUEST_TRIGGERS);
7600
+ var PULL_REQUEST_CI_WORKFLOW_LABEL = "Pull-request CI workflow";
7601
+ var WORKFLOW_WITHOUT_PULL_REQUEST_TRIGGER_LABEL = "CI workflow with no pull-request trigger";
7602
+ var WORKFLOW_TRIGGER_NOT_READ_LABEL = "CI workflow (trigger not read)";
7603
+ function b2WorkflowCitationBaseLabels(repoPath, file) {
7604
+ if (!/^\.github\/workflows\//.test(file)) return [WORKFLOW_TRIGGER_NOT_READ_LABEL];
7605
+ const fullPath = join(repoPath, file);
7606
+ const triggers = isRegularFile(fullPath) ? githubWorkflowTriggers(withoutContentReadAttribution(() => readRepoText(fullPath, "utf8"))) : null;
7607
+ if (!triggers) return [WORKFLOW_TRIGGER_NOT_READ_LABEL];
7608
+ const names = [...triggers.keys()];
7609
+ if (names.some((name) => PULL_REQUEST_TRIGGER_NAMES.has(name))) {
7610
+ return [PULL_REQUEST_CI_WORKFLOW_LABEL];
7611
+ }
7612
+ const named = [3, 2, 1].filter((shown) => shown <= names.length).map(
7613
+ (shown) => `${WORKFLOW_WITHOUT_PULL_REQUEST_TRIGGER_LABEL} (on: ${names.slice(0, shown).join(", ")}${names.length > shown ? ", ..." : ""})`
7614
+ );
7615
+ return [...named, WORKFLOW_WITHOUT_PULL_REQUEST_TRIGGER_LABEL];
7616
+ }
7617
+ function fittedCiWorkflowCitationLabel(candidates, label) {
7618
+ for (const candidate of candidates) {
7619
+ const full = label(candidate);
7620
+ if (full.length <= EVIDENCE_LABEL_MAX_LENGTH) return full;
7621
+ }
7622
+ return label(candidates[candidates.length - 1] ?? WORKFLOW_TRIGGER_NOT_READ_LABEL);
7623
+ }
7624
+ var PRINT_ONLY_RUN_LINE_PATTERN = /^(?:(?:echo|printf)(?:[ \t][^|&;`$()<>\\]*)?|true|:|exit[ \t]+0)$/;
7625
+ var BLOCK_SCALAR_INDICATOR_PATTERN = /^[|>][-+0-9]*$/;
7626
+ var PRINT_ONLY_JOB_KEYS = /* @__PURE__ */ new Set(["runs-on", "name", "timeout-minutes", "steps"]);
7627
+ var PRINT_ONLY_TOP_LEVEL_KEY_PATTERN = /^(?:name|run-name|on|true|permissions|concurrency|jobs)[ \t]*:/;
7628
+ var OTHER_LEADING_BLANK_PATTERN = /^[ \t]*[\v\f\u00a0\u1680\u2000-\u200a\u202f\u205f\u3000\ufeff]/m;
7629
+ var PRINT_ONLY_STEP_KEYS = /* @__PURE__ */ new Set(["name", "uses", "run"]);
7630
+ var UNQUOTED_KEY_PATTERN = /^([a-z][a-z-]*):(?:[ \t]+([^]*))?$/;
7631
+ var CHECKOUT_ACTION_PATTERN = /^actions\/checkout@[A-Za-z0-9._/-]+$/;
7632
+ function isPrintOnlyShellLine(text) {
7633
+ const command = text.trim();
7634
+ const doubles = command.split('"').length - 1;
7635
+ const singles = command.split("'").length - 1;
7636
+ const balanced = (doubles === 0 || singles === 0) && doubles % 2 === 0 && singles % 2 === 0;
7637
+ return balanced && PRINT_ONLY_RUN_LINE_PATTERN.test(command);
7638
+ }
7639
+ function isOneLineScalar(value) {
7640
+ const quote = value[0];
7641
+ if (quote !== '"' && quote !== "'") return true;
7642
+ return value.length > 1 && value.endsWith(quote);
7643
+ }
7644
+ function isPrintOnlyWorkflow(contents) {
7645
+ if (YAML_LINE_BREAK_NOT_SPLIT_PATTERN.test(contents) || OTHER_LEADING_BLANK_PATTERN.test(contents)) return false;
7646
+ const lines = contents.split(/\r?\n/);
7647
+ const isContent = (line) => line.trim() !== "" && !line.trimStart().startsWith("#");
7648
+ let jobsStart = -1;
7649
+ for (let index = 0; index < lines.length; index += 1) {
7650
+ const raw = lines[index] ?? "";
7651
+ if (!isContent(raw) || /^[ \t]/.test(raw)) continue;
7652
+ if (!PRINT_ONLY_TOP_LEVEL_KEY_PATTERN.test(raw)) return false;
7653
+ if (/^jobs:[ \t]*$/.test(stripYamlComment(raw))) {
7654
+ if (jobsStart !== -1) return false;
7655
+ jobsStart = index;
7656
+ }
7657
+ }
7658
+ if (jobsStart === -1) return false;
7659
+ let jobIndent = -1;
7660
+ let keyIndent = -1;
7661
+ let inSteps = false;
7662
+ let stepKeyColumn = -1;
7663
+ let step = null;
7664
+ let body = null;
7665
+ let runSteps = 0;
7666
+ const closeStep = () => {
7667
+ if (step && step.run && step.uses) return false;
7668
+ if (step?.run) runSteps += 1;
7669
+ step = null;
7670
+ return true;
5826
7671
  };
7672
+ for (let index = jobsStart + 1; index < lines.length; index += 1) {
7673
+ const raw = lines[index] ?? "";
7674
+ if (raw.trim() === "") continue;
7675
+ const indent = lineIndent(raw);
7676
+ if (body) {
7677
+ if (indent > body.column) {
7678
+ if (!body.block) return false;
7679
+ const text2 = raw.trim();
7680
+ if (!text2.startsWith("#") && !isPrintOnlyShellLine(text2)) return false;
7681
+ continue;
7682
+ }
7683
+ body = null;
7684
+ }
7685
+ if (!isContent(raw)) continue;
7686
+ const text = stripYamlComment(raw).trimEnd();
7687
+ if (indent === 0) break;
7688
+ if (jobIndent === -1) jobIndent = indent;
7689
+ if (indent < jobIndent) return false;
7690
+ if (indent === jobIndent) {
7691
+ if (!closeStep()) return false;
7692
+ if (!/^[ \t]*[A-Za-z_][A-Za-z0-9_-]*:[ \t]*$/.test(text)) return false;
7693
+ keyIndent = -1;
7694
+ inSteps = false;
7695
+ continue;
7696
+ }
7697
+ if (keyIndent === -1) keyIndent = indent;
7698
+ const isItem = /^[ \t]*-[ \t]+/.test(text);
7699
+ if (indent < keyIndent) return false;
7700
+ if (indent === keyIndent && !(inSteps && isItem)) {
7701
+ if (!closeStep()) return false;
7702
+ inSteps = false;
7703
+ const key2 = UNQUOTED_KEY_PATTERN.exec(text.trim());
7704
+ if (!key2 || !PRINT_ONLY_JOB_KEYS.has(key2[1] ?? "")) return false;
7705
+ const value2 = key2[2] ?? "";
7706
+ if (key2[1] === "steps") {
7707
+ if (value2 !== "") return false;
7708
+ inSteps = true;
7709
+ continue;
7710
+ }
7711
+ if (value2 === "" || /^[[{&*|>!]/.test(value2) || !isOneLineScalar(value2)) return false;
7712
+ continue;
7713
+ }
7714
+ if (!inSteps) return false;
7715
+ let keyText = text;
7716
+ let column = indent;
7717
+ if (isItem) {
7718
+ if (!closeStep()) return false;
7719
+ const item = /^([ \t]*-[ \t]+)([^]*)$/.exec(text);
7720
+ keyText = item?.[2] ?? "";
7721
+ column = (item?.[1] ?? "").length;
7722
+ stepKeyColumn = column;
7723
+ step = {};
7724
+ } else if (indent !== stepKeyColumn || !step) {
7725
+ return false;
7726
+ }
7727
+ const key = UNQUOTED_KEY_PATTERN.exec(keyText.trim());
7728
+ if (!key || !PRINT_ONLY_STEP_KEYS.has(key[1] ?? "")) return false;
7729
+ const value = key[2] ?? "";
7730
+ const current = step ?? {};
7731
+ if (key[1] === "name") {
7732
+ if (value === "" || /^[[{&*|>!]/.test(value) || !isOneLineScalar(value)) return false;
7733
+ } else if (key[1] === "uses") {
7734
+ if (current.uses || !CHECKOUT_ACTION_PATTERN.test(value)) return false;
7735
+ current.uses = true;
7736
+ } else {
7737
+ if (current.run) return false;
7738
+ current.run = true;
7739
+ if (BLOCK_SCALAR_INDICATOR_PATTERN.test(value)) {
7740
+ body = { column, block: true };
7741
+ } else {
7742
+ if (value === "" || !isPrintOnlyShellLine(value) || /^["']/.test(value)) return false;
7743
+ body = { column, block: false };
7744
+ }
7745
+ }
7746
+ }
7747
+ if (!closeStep()) return false;
7748
+ return runSteps > 0;
7749
+ }
7750
+ function findB3UnshownCheckWorkflows(root, counted, describePath = (file) => file) {
7751
+ const printOnly = [];
7752
+ for (const file of counted.filter((path) => /^\.github\/workflows\//.test(path))) {
7753
+ const fullPath = join(root, file);
7754
+ if (!isRegularFile(fullPath)) continue;
7755
+ const contents = withoutContentReadAttribution(() => readRepoText(fullPath, "utf8"));
7756
+ if (isPrintOnlyWorkflow(contents)) printOnly.push(describePath(file));
7757
+ }
7758
+ return { printOnly };
7759
+ }
7760
+ function mergeB3UnshownCheckWorkflows(own, root) {
7761
+ return { printOnly: [...own.printOnly, ...root.printOnly] };
7762
+ }
7763
+ function b3UnshownCheckCount(unshown) {
7764
+ return new Set(unshown.printOnly).size;
7765
+ }
7766
+ function b3UnshownCheckDisclosure(unshown, budget = Number.POSITIVE_INFINITY) {
7767
+ const count = b3UnshownCheckCount(unshown);
7768
+ if (count === 0) return "";
7769
+ const tail = ` The PR-gate CI workflow count still includes ${count === 1 ? "it" : "them"}.`;
7770
+ const parts = (shown) => [
7771
+ `${unshown.printOnly.slice(0, shown).join(", ")}${unshown.printOnly.length > shown ? ` and ${unshown.printOnly.length - shown} more` : ""} only print${unshown.printOnly.length === 1 ? "s" : ""} text`
7772
+ ];
7773
+ for (let shown = 3; shown > 0; shown -= 1) {
7774
+ const sentence = `${WITAN_B3_UNSHOWN_CHECK_PREFIX}${parts(shown).join("; ")}.${tail}`;
7775
+ if (sentence.length <= budget) return sentence;
7776
+ }
7777
+ const countOnly = `${WITAN_B3_UNSHOWN_CHECK_PREFIX}${count} workflow${count === 1 ? "" : "s"}.${tail}`;
7778
+ return countOnly.length <= budget ? countOnly : "";
7779
+ }
7780
+ function b3UnshownCheckCondition(unshown) {
7781
+ const count = b3UnshownCheckCount(unshown);
7782
+ const files = unshown.printOnly.map((path) => `${basename(path)} only prints text`);
7783
+ const lead = `includes ${count} not shown to run a check`;
7784
+ for (let shown = Math.min(files.length, 3); shown > 0; shown -= 1) {
7785
+ const condition = `${lead}: ${files.slice(0, shown).join("; ")}${files.length > shown ? "; ..." : ""}`;
7786
+ if (condition.length <= METRIC_CONDITION_MAX_LENGTH) return condition;
7787
+ }
7788
+ return lead;
5827
7789
  }
5828
7790
  function auditFreshnessCommitterYearFromGitResult(result, scanLimitations) {
5829
7791
  if (!result.ok) {
@@ -6182,7 +8144,6 @@ function hasCredibleTestScript(scripts) {
6182
8144
  var CI_TEST_COMMAND_PATTERN = /\b(pytest|go\s+test|cargo\s+test|mvn\s+test|gradle\s+test|dotnet\s+test|ctest|make\s+test|jest|vitest|mocha|rspec|phpunit|pnpm\s+(?:run\s+)?test|npm\s+(?:run\s+)?test|yarn\s+(?:run\s+)?test|node\s+--test|node:test)\b|--target[=\s]+test\b/i;
6183
8145
  var CI_LINT_COMMAND_PATTERN = /\b(eslint|pylint|flake8|black|ruff)\b/i;
6184
8146
  var CI_TYPECHECK_COMMAND_PATTERN = /\b(tsc|mypy|pyright)\b/i;
6185
- var SCHEDULE_TRIGGER_PATTERN = /(^|\n)\s*schedule:\s*\r?\n\s*-\s*cron:/;
6186
8147
  var PUBLISHED_RESULT_MARKER_PATTERN = /actions\/deploy-pages|actions\/upload-pages-artifact|peaceiris\/actions-gh-pages|git\s+push\b|create-or-update-comment/i;
6187
8148
  var EPHEMERAL_ARTIFACT_ONLY_PATTERN = /actions\/upload-artifact/i;
6188
8149
  var SCHEDULED_HEALTH_PATH_PATTERN = /(?:health|smoke|monitor|status|uptime|canary)/i;
@@ -6500,7 +8461,7 @@ function parseGitTrackedFiles(repoPath, output, includeHardExcluded, registerWit
6500
8461
  continue;
6501
8462
  }
6502
8463
  try {
6503
- const stat = lstatSync(fullPath);
8464
+ const stat = lstatSync2(fullPath);
6504
8465
  if (!stat.isFile()) {
6505
8466
  recordContentSkip(fullPath, "non_regular_file", true);
6506
8467
  withhold(file, "non_regular_file");
@@ -6575,19 +8536,19 @@ function resolveMonorepoContext(repoPath, scanLimitations) {
6575
8536
  }
6576
8537
  const top = topResult.stdout.trim();
6577
8538
  if (!top) return null;
6578
- const rootResolved = resolve(top);
8539
+ const rootResolved = resolve2(top);
6579
8540
  const repoResolved = (() => {
6580
8541
  try {
6581
- return realpathSync(repoPath);
8542
+ return realpathSync2(repoPath);
6582
8543
  } catch {
6583
- return resolve(repoPath);
8544
+ return resolve2(repoPath);
6584
8545
  }
6585
8546
  })();
6586
8547
  if (rootResolved === repoResolved) return null;
6587
- if (!repoResolved.startsWith(rootResolved + sep)) return null;
8548
+ if (!repoResolved.startsWith(rootResolved + sep2)) return null;
6588
8549
  const rootFiles = listGitTrackedFiles(rootResolved, scanLimitations);
6589
8550
  if (!rootFiles || rootFiles.length === 0) return null;
6590
- const subRel = repoResolved.slice(rootResolved.length + 1).split(sep).join("/");
8551
+ const subRel = repoResolved.slice(rootResolved.length + 1).split(sep2).join("/");
6591
8552
  const subPrefix = subRel ? `${subRel}/` : "";
6592
8553
  const sharedFiles = rootFiles.filter(
6593
8554
  (file) => (isLockfile(file) || isCiWorkflow(file) || isDependencyUpdateConfig(file)) && (subPrefix === "" || !file.startsWith(subPrefix))
@@ -6620,7 +8581,7 @@ function visitRepoDir(repoPath, dirPath, files, rootDevice) {
6620
8581
  }
6621
8582
  if (entry.isDirectory()) {
6622
8583
  try {
6623
- if (lstatSync(fullPath, { bigint: true }).dev !== BigInt(rootDevice)) {
8584
+ if (lstatSync2(fullPath, { bigint: true }).dev !== BigInt(rootDevice)) {
6624
8585
  recordContentSkip(fullPath, "denied_path");
6625
8586
  continue;
6626
8587
  }
@@ -6631,7 +8592,7 @@ function visitRepoDir(repoPath, dirPath, files, rootDevice) {
6631
8592
  visitRepoDir(repoPath, fullPath, files, rootDevice);
6632
8593
  continue;
6633
8594
  }
6634
- const repoRelativePath = relative(repoPath, fullPath);
8595
+ const repoRelativePath = relative2(repoPath, fullPath);
6635
8596
  if (!entry.isFile()) {
6636
8597
  recordContentSkip(fullPath, "non_regular_file");
6637
8598
  registerWithheldRepoPath(repoRelativePath, "non_regular_file");
@@ -6911,7 +8872,7 @@ function hasLiveFrameworkAssertion({ commentFree, code }) {
6911
8872
  function hasFrameworkSuiteLevelSkip({ code }) {
6912
8873
  return DOTNET_IGNORED_CLASS_PATTERN.test(code) || SCALATEST_IMPORT_PATTERN.test(code) && SCALATEST_IGNORED_CLASS_PATTERN.test(code) || KOTEST_IMPORT_PATTERN.test(code) && KOTEST_IGNORED_CLASS_PATTERN.test(code);
6913
8874
  }
6914
- function measureNonHollowTestShare(repoPath, files, useV17Detectors = false) {
8875
+ function measureNonHollowTestShare(repoPath, files, useV17Detectors = false, runWideSkip) {
6915
8876
  let nonHollowCount = 0;
6916
8877
  let ratedCount = 0;
6917
8878
  for (const file of files) {
@@ -6921,16 +8882,193 @@ function measureNonHollowTestShare(repoPath, files, useV17Detectors = false) {
6921
8882
  let maskedSource;
6922
8883
  const masked = () => maskedSource ??= maskCommentsAndStrings(contents);
6923
8884
  const frameworkSuiteSkipped = /\bIgnored?\b/.test(contents) && hasFrameworkSuiteLevelSkip(masked());
6924
- const nonHollow = (JS_TEST_ASSERTION_PATTERN.test(contents) || CPP_TEST_ASSERTION_PATTERN.test(contents) || MULTI_ECOSYSTEM_TEST_ASSERTION_PATTERN.test(contents) || hasLiveAvaAssertion(contents) || hasLiveFrameworkAssertion(masked())) && !SUITE_LEVEL_SKIP_PATTERN.test(contents) && !frameworkSuiteSkipped;
8885
+ const asserting = JS_TEST_ASSERTION_PATTERN.test(contents) || CPP_TEST_ASSERTION_PATTERN.test(contents) || MULTI_ECOSYSTEM_TEST_ASSERTION_PATTERN.test(contents) || hasLiveAvaAssertion(contents) || hasLiveFrameworkAssertion(masked());
8886
+ const cannotRunAsTest = runWideSkip !== void 0 && (PYTEST_CONFTEST_PATTERN.test(file) || runWideSkip.disabledByRunWideSkip(file) || hasUnconditionalPytestModuleSkip(file, contents));
8887
+ const nonHollow = asserting && !SUITE_LEVEL_SKIP_PATTERN.test(contents) && !frameworkSuiteSkipped && !cannotRunAsTest;
6925
8888
  if (nonHollow) {
6926
8889
  nonHollowCount += 1;
6927
8890
  ratedCount += 1;
6928
- } else if (NAME_SHAPED_TEST_FILE_PATTERN.test(file) || useV17Detectors && NAME_SHAPED_TEST_FILE_PATTERN_V17.test(file) || frameworkSuiteSkipped && SUITE_SKIP_NAME_SHAPED_TEST_FILE_PATTERN.test(file)) {
8891
+ } else if (NAME_SHAPED_TEST_FILE_PATTERN.test(file) || useV17Detectors && NAME_SHAPED_TEST_FILE_PATTERN_V17.test(file) || frameworkSuiteSkipped && SUITE_SKIP_NAME_SHAPED_TEST_FILE_PATTERN.test(file) || cannotRunAsTest && asserting) {
6929
8892
  ratedCount += 1;
6930
8893
  }
6931
8894
  }
6932
8895
  return { nonHollowCount, ratedCount };
6933
8896
  }
8897
+ var PYTEST_CONFTEST_PATTERN = /(^|\/)conftest\.py$/;
8898
+ var PYTEST_MODULE_SKIP_MARK_PATTERN = /\bpytest\.mark\.skip\b(?!if)/;
8899
+ var PYTEST_MODULE_SKIP_ASSIGNMENT_PATTERN = /^pytestmark[ \t]*=(?!=)/;
8900
+ var PYTEST_MODULE_SKIP_MAX_STATEMENT_LINES = 20;
8901
+ function pythonCodeOutsideStrings(line) {
8902
+ let code = "";
8903
+ let quote = "";
8904
+ for (let index = 0; index < line.length; index += 1) {
8905
+ const character = line[index] ?? "";
8906
+ if (quote !== "") {
8907
+ if (character === "\\") index += 1;
8908
+ else if (character === quote) {
8909
+ code += character;
8910
+ quote = "";
8911
+ }
8912
+ continue;
8913
+ }
8914
+ if (character === "#") break;
8915
+ if (character === '"' || character === "'") quote = character;
8916
+ code += character;
8917
+ }
8918
+ return code;
8919
+ }
8920
+ var PYTEST_MARK_NAME_GLOBAL_PATTERN = /\bpytestmark\b/g;
8921
+ function countPytestMarkMentions(text) {
8922
+ return [...text.matchAll(PYTEST_MARK_NAME_GLOBAL_PATTERN)].length;
8923
+ }
8924
+ function pythonBracketDelta(code) {
8925
+ let depth = 0;
8926
+ for (const character of code) {
8927
+ if (character === "(" || character === "[" || character === "{") depth += 1;
8928
+ else if (character === ")" || character === "]" || character === "}") depth -= 1;
8929
+ }
8930
+ return depth;
8931
+ }
8932
+ function hasUnconditionalPytestModuleSkip(file, contents) {
8933
+ if (!/\.py$/.test(file)) return false;
8934
+ const lines = contents.split("\n");
8935
+ let mentionCount = 0;
8936
+ let start = -1;
8937
+ for (const [index, line] of lines.entries()) {
8938
+ const count = Math.max(
8939
+ countPytestMarkMentions(stripPythonLineComment(line)),
8940
+ countPytestMarkMentions(pythonCodeOutsideStrings(line))
8941
+ );
8942
+ if (count > 0 && start === -1) start = index;
8943
+ mentionCount += count;
8944
+ }
8945
+ if (mentionCount !== 1) return false;
8946
+ const first = lines[start] ?? "";
8947
+ if (!PYTEST_MODULE_SKIP_ASSIGNMENT_PATTERN.test(first) || !PYTEST_MODULE_SKIP_MARK_PATTERN.test(pythonCodeOutsideStrings(first))) {
8948
+ return false;
8949
+ }
8950
+ const statement = [];
8951
+ let depth = 0;
8952
+ for (let index = start; index < lines.length; index += 1) {
8953
+ if (statement.length === PYTEST_MODULE_SKIP_MAX_STATEMENT_LINES) return false;
8954
+ const raw = (lines[index] ?? "").replace(/\r$/, "");
8955
+ const code = pythonCodeOutsideStrings(raw);
8956
+ if (/\\[ \t]*$/.test(code) || /\\[ \t]*$/.test(raw)) return false;
8957
+ statement.push(stripPythonLineComment(raw));
8958
+ depth += pythonBracketDelta(code);
8959
+ if (depth <= 0) break;
8960
+ }
8961
+ if (depth > 0) return false;
8962
+ return !/\bif\b/.test(statement.join("\n"));
8963
+ }
8964
+ function stripPythonLineComment(line) {
8965
+ const hash = line.indexOf("#");
8966
+ return hash === -1 ? line : line.slice(0, hash);
8967
+ }
8968
+ function isGovernedPytestFile(skip, file) {
8969
+ if (!/\.py$/.test(file) || PYTEST_CONFTEST_PATTERN.test(file)) return false;
8970
+ return skip.scope === "" || file.startsWith(`${skip.scope}/`);
8971
+ }
8972
+ var PYTEST_COLLECTION_HOOK_PATTERN = /^def[ \t]+pytest_collection_modifyitems[ \t]*\(/;
8973
+ var PYTEST_COLLECTION_HOOK_NAME = "pytest_collection_modifyitems";
8974
+ var PYTEST_COLLECTION_HOOK_NAME_PATTERN = /\bpytest_collection_modifyitems\b/;
8975
+ var PYTEST_HOOK_CONDITIONAL_PATTERN = /^(?:if|elif|else|while|try|except|finally|with|match|case|return|break|continue|yield)\b|\bskipif\b|\bif\b/;
8976
+ var PYTEST_HOOK_FOR_EVERY_ITEM_PATTERN = /^for[ \t]+([A-Za-z_]\w*)[ \t]+in[ \t]+items[ \t]*:(.*)$/;
8977
+ var PYTEST_HOOK_REBIND_ITEMS_PATTERN = /^(?:del[ \t]+)?items[ \t]*(?:[-+*/%&|^@]?=(?!=)|,|$)|\bitems[ \t]*:=/;
8978
+ var PYTEST_HOOK_CLEAR_ITEMS_PATTERN = /^(?:items\.clear\(\s*\)|items\[\s*:\s*\][ \t]*=[ \t]*\[\s*\]|del[ \t]+items\[\s*:\s*\])/;
8979
+ var PYTEST_HOOK_REFILL_ITEMS_PATTERN = /^items(?:[ \t]*\.[ \t]*(?:extend|append|insert)[ \t]*\(|[ \t]*\+=|[ \t]*\[[^\]]*\][ \t]*=(?!=))/;
8980
+ var PYTEST_HOOK_EMPTY_REFILL_PATTERN = /^items(?:[ \t]*\.[ \t]*extend[ \t]*\([ \t]*|[ \t]*\+=[ \t]*)(?:\[[ \t]*\]|\([ \t]*\))[ \t]*(?:\)[ \t]*)?$/;
8981
+ function pythonLineStatements(line) {
8982
+ if (!line.includes(";") || /['"]/.test(line)) return [line];
8983
+ return line.split(";").map((statement) => statement.trim()).filter((statement) => statement !== "");
8984
+ }
8985
+ var PYTEST_HOOK_SKIP_MARKER_PATTERN = /^([A-Za-z_]\w*)[ \t]*\.[ \t]*add_marker[ \t]*\([ \t]*pytest\.mark\.skip\b(?!if)/;
8986
+ var PYTHON_ASSIGNMENT_TARGET_PATTERN = /^(?:del[ \t]+)?([A-Za-z_]\w*)[ \t]*(?:[-+*/%&|^@]?=(?!=)|,)/;
8987
+ var PYTHON_IDENTIFIER_PATTERN = /[A-Za-z_]\w*/g;
8988
+ function containsPythonIdentifier(text, name) {
8989
+ for (const match of text.matchAll(PYTHON_IDENTIFIER_PATTERN)) if (match[0] === name) return true;
8990
+ return false;
8991
+ }
8992
+ function mayRebindPythonName(text, name) {
8993
+ if (PYTHON_ASSIGNMENT_TARGET_PATTERN.exec(text)?.[1] === name) return true;
8994
+ const code = pythonCodeOutsideStrings(text);
8995
+ return /\bfor\b|\bas\b|:=/.test(code) && containsPythonIdentifier(code, name);
8996
+ }
8997
+ function hookSkipsEveryItem(body) {
8998
+ if (body.some(({ text }) => PYTEST_HOOK_REBIND_ITEMS_PATTERN.test(text))) return false;
8999
+ const hookIndent = body[0]?.indent;
9000
+ const skips = (text, name) => PYTEST_HOOK_SKIP_MARKER_PATTERN.exec(text)?.[1] === name;
9001
+ for (let index = 0; index < body.length; index += 1) {
9002
+ const statement = body[index];
9003
+ if (statement.indent !== hookIndent) continue;
9004
+ const loop = PYTEST_HOOK_FOR_EVERY_ITEM_PATTERN.exec(statement.text);
9005
+ if (!loop) continue;
9006
+ const name = loop[1] ?? "";
9007
+ const inline = (loop[2] ?? "").trim();
9008
+ if (inline !== "") {
9009
+ if (skips(inline, name)) return true;
9010
+ continue;
9011
+ }
9012
+ const childIndent = body[index + 1]?.indent;
9013
+ if (childIndent === void 0 || childIndent <= statement.indent) continue;
9014
+ let skipped = false;
9015
+ let rebound = false;
9016
+ for (let inner = index + 1; inner < body.length; inner += 1) {
9017
+ const { indent, text } = body[inner];
9018
+ if (indent <= statement.indent) break;
9019
+ if (mayRebindPythonName(text, name)) rebound = true;
9020
+ if (indent === childIndent && skips(text, name)) skipped = true;
9021
+ }
9022
+ if (skipped && !rebound) return true;
9023
+ }
9024
+ return false;
9025
+ }
9026
+ function findPytestRunWideSkips(repoPath, repoFiles) {
9027
+ const found = [];
9028
+ for (const file of repoFiles) {
9029
+ if (!PYTEST_CONFTEST_PATTERN.test(file) || GENERATED_OR_VENDOR_PATH_PATTERN.test(file)) continue;
9030
+ const fullPath = join(repoPath, file);
9031
+ if (!isRegularFile(fullPath)) continue;
9032
+ const lines = readRepoText(fullPath, "utf8").split("\n");
9033
+ let last = -1;
9034
+ for (let index = 0; index < lines.length; index += 1) {
9035
+ const line = lines[index] ?? "";
9036
+ if (PYTEST_COLLECTION_HOOK_PATTERN.test(line)) {
9037
+ last = index;
9038
+ } else if (line.includes(PYTEST_COLLECTION_HOOK_NAME) && !line.trimStart().startsWith("#") && PYTEST_COLLECTION_HOOK_NAME_PATTERN.test(line)) {
9039
+ last = -1;
9040
+ break;
9041
+ }
9042
+ }
9043
+ for (let index = last; index !== -1; index = -1) {
9044
+ const indented = [];
9045
+ for (let next = index + 1; next < lines.length; next += 1) {
9046
+ const raw = lines[next] ?? "";
9047
+ const trimmed = raw.trim();
9048
+ if (trimmed === "" || trimmed.startsWith("#")) continue;
9049
+ const indent = raw.length - raw.trimStart().length;
9050
+ if (indent === 0) break;
9051
+ for (const text of pythonLineStatements(stripPythonLineComment(trimmed).trim())) {
9052
+ indented.push({ indent, text });
9053
+ }
9054
+ }
9055
+ const body = indented.map(({ text }) => text);
9056
+ if (body.some((statement) => PYTEST_HOOK_CONDITIONAL_PATTERN.test(statement))) continue;
9057
+ const hookIndent = indented[0]?.indent;
9058
+ const clears = indented.some(
9059
+ ({ indent, text }) => indent === hookIndent && PYTEST_HOOK_CLEAR_ITEMS_PATTERN.test(text)
9060
+ );
9061
+ const refills = body.some(
9062
+ (statement) => !PYTEST_HOOK_CLEAR_ITEMS_PATTERN.test(statement) && !PYTEST_HOOK_EMPTY_REFILL_PATTERN.test(statement) && PYTEST_HOOK_REFILL_ITEMS_PATTERN.test(statement)
9063
+ );
9064
+ const kind = clears && !refills ? "clear" : hookSkipsEveryItem(indented) ? "skip" : void 0;
9065
+ if (!kind) continue;
9066
+ const scope = dirname2(file);
9067
+ found.push({ path: file, line: index + 1, scope: scope === "." ? "" : scope, kind });
9068
+ }
9069
+ }
9070
+ return found;
9071
+ }
6934
9072
  function countFilesContaining(repoPath, files, pattern) {
6935
9073
  return files.filter((file) => fileContains(repoPath, file, pattern)).length;
6936
9074
  }
@@ -7107,10 +9245,10 @@ function readPyprojectDependencySpecs(path) {
7107
9245
  const dep = rawDep.replace(/["'\s]/g, "");
7108
9246
  if (!dep) continue;
7109
9247
  const nameMatch = /^([A-Za-z0-9][\w.-]*)/.exec(dep);
7110
- const versionMatch = /([><=~!^]+[\d.]+)/.exec(dep);
9248
+ const versionConstraint = firstVersionConstraint(dep);
7111
9249
  const depName = nameMatch?.[1];
7112
9250
  if (depName) {
7113
- specs.push({ name: depName, version: versionMatch?.[1] ?? dep });
9251
+ specs.push({ name: depName, version: versionConstraint ?? dep });
7114
9252
  }
7115
9253
  }
7116
9254
  }
@@ -7884,7 +10022,7 @@ function isClaimArtifactDeclaringWorkflow(file) {
7884
10022
  function normaliseDeclaredClaimArtifactPath(candidate, baseDir) {
7885
10023
  if (!DECLARED_CLAIM_ARTIFACT_PATH_PATTERN.test(candidate)) return null;
7886
10024
  const joined = candidate.startsWith("/") ? posix.normalize(candidate.slice(1)) : posix.normalize(posix.join(baseDir, candidate));
7887
- const path = joined.replace(/\/+$/, "");
10025
+ const path = stripTrailingSlashes(joined);
7888
10026
  if (path === "" || path === "." || path === ".." || path.startsWith("../") || path.startsWith("/")) {
7889
10027
  return null;
7890
10028
  }
@@ -7894,7 +10032,7 @@ function markdownLinkTarget(target) {
7894
10032
  if (/^[a-z][a-z0-9+.-]*:/i.test(target) || target.startsWith("//") || target.startsWith("#")) {
7895
10033
  return null;
7896
10034
  }
7897
- const withoutSuffix = target.replace(/[?#].*$/, "");
10035
+ const withoutSuffix = stripFromQueryOrFragment(target);
7898
10036
  try {
7899
10037
  return decodeURIComponent(withoutSuffix);
7900
10038
  } catch {
@@ -7969,7 +10107,7 @@ function workflowClaimArtifactDeclarations(contents) {
7969
10107
  writeContinues = false;
7970
10108
  };
7971
10109
  contents.split(/\r?\n/).forEach((rawLine, index) => {
7972
- const line = rawLine.replace(/(^|\s)#.*$/, "");
10110
+ const line = stripHashCommentAtStartOrAfterBlank(rawLine);
7973
10111
  if (line.trim() === "") return;
7974
10112
  const indent = line.length - line.trimStart().length;
7975
10113
  if (stepsIndent !== null && (indent < stepsIndent || indent === stepsIndent && !line.trimStart().startsWith("- "))) {
@@ -8093,7 +10231,7 @@ function vitestCoveragePropertyCollects(contents) {
8093
10231
  return true;
8094
10232
  }
8095
10233
  function viteConfigCollectsCoverage(repoPath, file) {
8096
- const directory = dirname(file);
10234
+ const directory = dirname2(file);
8097
10235
  for (const extension of ["ts", "mts", "cts", "js", "mjs", "cjs"]) {
8098
10236
  const sibling = join(repoPath, directory, `vitest.config.${extension}`);
8099
10237
  if (!isRegularFile(sibling)) continue;
@@ -8793,7 +10931,7 @@ function containsPotentialCoverageFlag(command) {
8793
10931
  function reachableShellWrapper(words, repoFiles) {
8794
10932
  const executable = basename(words[0] ?? "").toLowerCase();
8795
10933
  const candidate = /^(?:ba|da|k|z)?sh$/u.test(executable) ? words[1] : words[0];
8796
- if (!candidate || !/(?:^|\/)\.?[^\s]*\.sh$/i.test(candidate)) return void 0;
10934
+ if (!candidate || !isShellScriptPath(candidate)) return void 0;
8797
10935
  const normalized = candidate.replace(/^\.\//u, "");
8798
10936
  if (normalized.startsWith("/") || normalized.split("/").includes("..")) return void 0;
8799
10937
  return repoFiles.has(normalized) ? normalized : void 0;
@@ -9361,7 +11499,7 @@ function packageJsonDescribesPackagedApplication(repoPath, manifest, repoFiles)
9361
11499
  return hasDesktopRuntime && hasPackagingCommand && hasApplicationMetadata;
9362
11500
  }
9363
11501
  function findTauriApplicationMetadata(repoPath, manifest, repoFiles) {
9364
- const manifestDirectory = dirname(manifest);
11502
+ const manifestDirectory = dirname2(manifest);
9365
11503
  const prefix = manifestDirectory === "." ? "" : `${manifestDirectory}/`;
9366
11504
  const candidates = [`${prefix}src-tauri/tauri.conf.json`, `${prefix}tauri.conf.json`];
9367
11505
  return candidates.some((file) => {
@@ -9504,7 +11642,7 @@ function readPnpmWorkspacePatterns(yamlPath) {
9504
11642
  return patterns;
9505
11643
  }
9506
11644
  function stripYamlLineComment(text) {
9507
- return /^\s*#/.test(text) ? "" : text.replace(/\s+#.*$/, "");
11645
+ return /^\s*#/.test(text) ? "" : stripTrailingHashComment(text);
9508
11646
  }
9509
11647
  function yamlScalar(raw) {
9510
11648
  const single = /^'((?:[^']|'')*)'\s*(?:#.*)?$/.exec(raw);
@@ -9518,7 +11656,7 @@ var WORKSPACE_GLOB_MAX_LENGTH = 256;
9518
11656
  var WORKSPACE_GLOB_MAX_SEGMENTS = 32;
9519
11657
  function workspaceGlobMatcher(pattern) {
9520
11658
  if (pattern.length > WORKSPACE_GLOB_MAX_LENGTH) return null;
9521
- const normalised = pattern.trim().replace(/^\.\//, "").replace(/\/+$/, "");
11659
+ const normalised = stripTrailingSlashes(pattern.trim().replace(/^\.\//, ""));
9522
11660
  if (normalised === "" || normalised.startsWith("/") || /(^|\/)\.\.?(\/|$)/.test(normalised) || /[[\]{}()!+@\\]/.test(normalised)) {
9523
11661
  return null;
9524
11662
  }
@@ -9627,7 +11765,7 @@ function countDelegatedCiSteps(root, workflows) {
9627
11765
  continue;
9628
11766
  }
9629
11767
  if (!uses.startsWith("./") || /^\.\/\.github\/workflows\//.test(uses)) continue;
9630
- const directory = uses.slice(2).replace(/\/+$/u, "");
11768
+ const directory = stripTrailingSlashes(uses.slice(2));
9631
11769
  if (directory.length === 0 || directory.split("/").includes("..")) continue;
9632
11770
  if (isRegularFile(join(root, directory, "action.yml")) || isRegularFile(join(root, directory, "action.yaml"))) {
9633
11771
  compositeActions += 1;
@@ -9647,7 +11785,7 @@ function extractWorkflowUsesValues(contents) {
9647
11785
  }
9648
11786
  const match = /^\s*(?:-\s*)?uses\s*:\s*(.*)$/u.exec(line);
9649
11787
  if (match) {
9650
- const value = (match[1] ?? "").replace(/\s+#.*$/u, "").trim();
11788
+ const value = stripTrailingHashComment(match[1] ?? "").trim();
9651
11789
  const quoted = /^(?:"(.*)"|'(.*)')$/u.exec(value);
9652
11790
  values.push(quoted ? quoted[1] ?? quoted[2] ?? "" : value);
9653
11791
  continue;
@@ -9907,7 +12045,7 @@ function stripUnbalancedShellParentheses(segment) {
9907
12045
  const opens = segment.split("(").length - 1;
9908
12046
  const closes = segment.split(")").length - 1;
9909
12047
  if (opens > closes) return segment.replace(/^\(+\s*/u, "");
9910
- if (closes > opens) return segment.replace(/\s*\)+$/u, "");
12048
+ if (closes > opens) return stripTrailingCloseParentheses(segment);
9911
12049
  return segment;
9912
12050
  }
9913
12051
  function strictShellWords(segment) {
@@ -9945,7 +12083,7 @@ function workspacePackageScriptsDisclosure(repoFiles, rootPackageJson, workspace
9945
12083
  }
9946
12084
  function isRegularFile(path) {
9947
12085
  try {
9948
- return lstatSync(path).isFile();
12086
+ return lstatSync2(path).isFile();
9949
12087
  } catch {
9950
12088
  return false;
9951
12089
  }
@@ -9955,6 +12093,11 @@ function fileContains(repoPath, file, pattern) {
9955
12093
  if (!isRegularFile(fullPath)) return false;
9956
12094
  return pattern.test(readRepoText(fullPath, "utf8"));
9957
12095
  }
12096
+ function fileSatisfies(repoPath, file, test) {
12097
+ const fullPath = join(repoPath, file);
12098
+ if (!isRegularFile(fullPath)) return false;
12099
+ return test(readRepoText(fullPath, "utf8"));
12100
+ }
9958
12101
  var HASH_COMMENT_SOURCE_PATTERN = /\.(?:py|rb)$/i;
9959
12102
  function stripCodeComments(file, contents) {
9960
12103
  const withoutComments = contents.replace(/\/\*[\s\S]*?\*\//g, (block) => block.replace(/[^\n]/g, "")).replace(/(?<!:)\/\/.*$/gm, "");
@@ -10013,7 +12156,7 @@ ${contextLines}`
10013
12156
  line,
10014
12157
  valueLength: value.length,
10015
12158
  characterClasses: characterClasses || "other",
10016
- valueFingerprint: createHash3("sha256").update(value).digest("hex"),
12159
+ valueFingerprint: createHash4("sha256").update(value).digest("hex"),
10017
12160
  kind: "pem_private_key"
10018
12161
  };
10019
12162
  }
@@ -10034,7 +12177,7 @@ function findPemPrivateKeyAssignment(contents, file) {
10034
12177
  if (!isBarePemKeyFile(file)) return null;
10035
12178
  return scanForPemPrivateKeyMatch(BARE_PEM_PRIVATE_KEY_PATTERN, contents, lines, false);
10036
12179
  }
10037
- function findCommittedSecretInFile(repoPath, file, useV36Detectors = false, useV39Detectors = false, useV47Detectors = false, useV23PemPrivateKeyGrammar = false) {
12180
+ function findCommittedSecretInFile(repoPath, file, useV36Detectors = false, useV39Detectors = false, useV47Detectors = false, useV23PemPrivateKeyGrammar = false, setAside) {
10038
12181
  const fullPath = join(repoPath, file);
10039
12182
  if (!isRegularFile(fullPath)) return null;
10040
12183
  const contents = readRepoText(fullPath, "utf8");
@@ -10047,7 +12190,12 @@ function findCommittedSecretInFile(repoPath, file, useV36Detectors = false, useV
10047
12190
  useV47Detectors
10048
12191
  );
10049
12192
  const secret = findRealSecretAssignment(secretScanContents, /* @__PURE__ */ new Set(), {
10050
- allowCredentialNamedDigest: useV36Detectors
12193
+ allowCredentialNamedDigest: useV36Detectors,
12194
+ ...setAside ? {
12195
+ observeValue: (value, outcome) => {
12196
+ if (isPlaceholderSetAsideSecret(value, outcome)) setAside.count += 1;
12197
+ }
12198
+ } : {}
10051
12199
  });
10052
12200
  if (secret) return secret;
10053
12201
  if (useV23PemPrivateKeyGrammar) {
@@ -10062,7 +12210,7 @@ function findCommittedSecretInFile(repoPath, file, useV36Detectors = false, useV
10062
12210
  }
10063
12211
  return useV36Detectors && !isTestCredentialConfiguration ? findDefaultAdministrativeCredential(secretScanContents) : null;
10064
12212
  }
10065
- function findV24ClassifiedSecretInFile(repoPath, file, useV36Detectors, useV39Detectors, useV47Detectors) {
12213
+ function findV24ClassifiedSecretInFile(repoPath, file, useV36Detectors, useV39Detectors, useV47Detectors, setAside) {
10066
12214
  const fullPath = join(repoPath, file);
10067
12215
  if (!isRegularFile(fullPath)) return null;
10068
12216
  const contents = readRepoText(fullPath, "utf8");
@@ -10076,7 +12224,7 @@ function findV24ClassifiedSecretInFile(repoPath, file, useV36Detectors, useV39De
10076
12224
  true
10077
12225
  );
10078
12226
  const rawLines = contents.split(/\r?\n/);
10079
- const classifyFor = (wanted) => (value, identifier, line) => {
12227
+ const classifyFor = (wanted, countSetAside = false) => (value, identifier, line) => {
10080
12228
  const outcome = classifyV24SecretShapedValue(
10081
12229
  value,
10082
12230
  identifier,
@@ -10084,11 +12232,14 @@ function findV24ClassifiedSecretInFile(repoPath, file, useV36Detectors, useV39De
10084
12232
  rawLines,
10085
12233
  useV36Detectors
10086
12234
  );
12235
+ if (countSetAside && setAside && isPlaceholderSetAsideSecret(value, outcome)) {
12236
+ setAside.count += 1;
12237
+ }
10087
12238
  return outcome.classification === wanted ? outcome : V24_NO_FINDING;
10088
12239
  };
10089
12240
  const real = findRealSecretAssignment(secretScanContents, /* @__PURE__ */ new Set(), {
10090
12241
  allowCredentialNamedDigest: useV36Detectors,
10091
- classifyValue: classifyFor("real")
12242
+ classifyValue: classifyFor("real", true)
10092
12243
  });
10093
12244
  if (real) return { match: real, classification: "real" };
10094
12245
  if (useV39Detectors && isEnvHistoryPath(file, useV36Detectors)) {
@@ -10280,8 +12431,9 @@ function findRealSecretAssignments(contents, excludedValueFingerprints = /* @__P
10280
12431
  const value = contents.slice(valueStart, cursor).replace(/[,;)]$/, "");
10281
12432
  nextSearchIndex = cursor;
10282
12433
  const outcome = options.classifyValue ? options.classifyValue(value, identifier, line) : !isPlaceholderSecretValue(value) && (looksLikeSecretValue(value) || options.allowCredentialNamedDigest === true && isLikelyDigestOrHash(value) && isExplicitCredentialIdentifier(identifier)) ? V24_REAL : V24_NO_FINDING;
12434
+ options.observeValue?.(value, outcome);
10283
12435
  if (outcome.classification !== "no_finding") {
10284
- const valueFingerprint = createHash3("sha256").update(value).digest("hex");
12436
+ const valueFingerprint = createHash4("sha256").update(value).digest("hex");
10285
12437
  const characterClasses = [
10286
12438
  /[a-z]/.test(value) ? "lower" : "",
10287
12439
  /[A-Z]/.test(value) ? "upper" : "",
@@ -10338,7 +12490,7 @@ function findWeakExplicitEnvCredential(contents) {
10338
12490
  line: index + 1,
10339
12491
  valueLength: value.length,
10340
12492
  characterClasses: characterClasses || "other",
10341
- valueFingerprint: createHash3("sha256").update(value).digest("hex")
12493
+ valueFingerprint: createHash4("sha256").update(value).digest("hex")
10342
12494
  };
10343
12495
  }
10344
12496
  return null;
@@ -10378,7 +12530,7 @@ function findDatabaseUrlUserinfoCredential(contents) {
10378
12530
  line: index + 1,
10379
12531
  valueLength: password.length,
10380
12532
  characterClasses: characterClasses || "other",
10381
- valueFingerprint: createHash3("sha256").update(password).digest("hex")
12533
+ valueFingerprint: createHash4("sha256").update(password).digest("hex")
10382
12534
  };
10383
12535
  }
10384
12536
  return null;
@@ -10397,7 +12549,7 @@ function findDefaultAdministrativeCredential(contents) {
10397
12549
  line: index + 1,
10398
12550
  valueLength: value.length,
10399
12551
  characterClasses: "lower",
10400
- valueFingerprint: createHash3("sha256").update(value).digest("hex"),
12552
+ valueFingerprint: createHash4("sha256").update(value).digest("hex"),
10401
12553
  kind: "default_admin"
10402
12554
  };
10403
12555
  }
@@ -10673,6 +12825,45 @@ function isPlaceholderSecretValue(value) {
10673
12825
  const normalized = value.trim().toLowerCase();
10674
12826
  return PLACEHOLDER_SECRET_PATTERN.test(normalized) || normalized.includes("your-") || normalized.includes("example") || ALL_CAPS_SNAKE_PLACEHOLDER_PATTERN.test(value.trim()) || isSyntheticFixtureSecretValue(normalized) || isFillerRunSecretValue(normalized);
10675
12827
  }
12828
+ function isPlaceholderSetAsideSecret(value, outcome) {
12829
+ return outcome.classification === "no_finding" && isPlaceholderSecretValue(value) && looksLikeSecretValue(value);
12830
+ }
12831
+ var PLACEHOLDER_SET_ASIDE_PATHS_SHOWN = 3;
12832
+ function placeholderSetAsidePathList(files, shown) {
12833
+ const named = files.slice(0, shown).map((file) => file.path);
12834
+ const more = files.length - named.length;
12835
+ return more > 0 ? `${named.join(", ")} and ${more} more file${more === 1 ? "" : "s"}` : named.join(", ");
12836
+ }
12837
+ function placeholderSetAsideValues(files) {
12838
+ const total = files.reduce((sum, file) => sum + file.count, 0);
12839
+ return `${total} credential-shaped value${total === 1 ? "" : "s"}`;
12840
+ }
12841
+ function placeholderSetAsideDisclosure(files, budget = Number.POSITIVE_INFINITY) {
12842
+ const total = files.reduce((sum, file) => sum + file.count, 0);
12843
+ const lead = `${WITAN_A2_PLACEHOLDER_SET_ASIDE_PREFIX}${placeholderSetAsideValues(files)} as ${total === 1 ? "a documentation placeholder because it contains" : "documentation placeholders because they contain"} a placeholder marker such as "example", so ${total === 1 ? "it was" : "they were"} not counted against secret cleanliness or checked further`;
12844
+ for (let shown = Math.min(files.length, PLACEHOLDER_SET_ASIDE_PATHS_SHOWN); shown > 0; shown -= 1) {
12845
+ const sentence = `${lead}: ${placeholderSetAsidePathList(files, shown)}.`;
12846
+ if (sentence.length <= budget) return sentence;
12847
+ }
12848
+ const countOnly = `${lead}, in ${files.length} file${files.length === 1 ? "" : "s"}.`;
12849
+ return countOnly.length <= budget ? countOnly : "";
12850
+ }
12851
+ function placeholderSetAsideCondition(files) {
12852
+ const lead = `${placeholderSetAsideValues(files)} set aside as documentation placeholders, not counted`;
12853
+ for (let shown = Math.min(files.length, PLACEHOLDER_SET_ASIDE_PATHS_SHOWN); shown > 0; shown -= 1) {
12854
+ const condition = `${lead}: ${placeholderSetAsidePathList(files, shown)}`;
12855
+ if (condition.length <= METRIC_CONDITION_MAX_LENGTH) return condition;
12856
+ }
12857
+ return `${lead} (${files.length} file${files.length === 1 ? "" : "s"})`;
12858
+ }
12859
+ var METRIC_CONDITION_MAX_LENGTH = 240;
12860
+ var CRITERION_NOTES_MAX_LENGTH = 1e3;
12861
+ var CRITERION_DISCLOSURE_MAX_LENGTH = 1e3;
12862
+ function placeholderSetAsideDisclosures2(files) {
12863
+ if (files.length === 0) return {};
12864
+ const sentence = placeholderSetAsideDisclosure(files, CRITERION_DISCLOSURE_MAX_LENGTH);
12865
+ return sentence ? { disclosures: [sentence] } : {};
12866
+ }
10676
12867
  function isFillerRunSecretValue(value) {
10677
12868
  const candidates = [value];
10678
12869
  const sepIdx = Math.max(value.lastIndexOf("_"), value.lastIndexOf("-"));
@@ -10849,7 +13040,7 @@ function hasSuspiciousDependencies(repoPath, manifest, useV47Detectors = false)
10849
13040
  }
10850
13041
  function readRecentCommits(repoPath) {
10851
13042
  const historyResult = execGit(
10852
- ["log", "--no-show-signature", "--max-count=12", "--format=%H%x00%s"],
13043
+ ["log", "--no-show-signature", `--max-count=${RECENT_COMMIT_WINDOW}`, "--format=%H%x00%s"],
10853
13044
  {
10854
13045
  cwd: repoPath
10855
13046
  }
@@ -10885,7 +13076,7 @@ function evidenceForRelative(repoPath, path, kind, label) {
10885
13076
  path,
10886
13077
  ...contents ? {
10887
13078
  line: firstMeaningfulLine(contents),
10888
- contentHash: createHash3("sha256").update(contents).digest("hex")
13079
+ contentHash: createHash4("sha256").update(contents).digest("hex")
10889
13080
  } : {}
10890
13081
  };
10891
13082
  }
@@ -10897,7 +13088,7 @@ function evidenceForRelativeAtLine(repoPath, path, kind, label, line) {
10897
13088
  label,
10898
13089
  path,
10899
13090
  line,
10900
- ...contents ? { contentHash: createHash3("sha256").update(contents).digest("hex") } : {}
13091
+ ...contents ? { contentHash: createHash4("sha256").update(contents).digest("hex") } : {}
10901
13092
  };
10902
13093
  }
10903
13094
  function metric(name, label, value, max, weight, unit, description, kind, presentation) {
@@ -10963,15 +13154,6 @@ function hasEnvPathInGitHistory(repoPath, scanLimitations) {
10963
13154
  // src/witan/public-scan.ts
10964
13155
  init_esm_shims();
10965
13156
 
10966
- // src/witan/history-depth.ts
10967
- init_esm_shims();
10968
- var SHALLOW_HISTORY_LIMITATION = "This repository is a shallow clone (git rev-parse --is-shallow-repository reports true), so its commit history is truncated. A2 (credentials and .env files in earlier commits) and B2 (pull-request merge ratio over recent commits) read history and saw only the commits present; on a full clone their results may differ. Scores are computed exactly as for any other scan. Re-scan a full clone (for actions/checkout, set fetch-depth: 0).";
10969
- function detectHistoryLimitations(repoPath) {
10970
- const result = execGit(["rev-parse", "--is-shallow-repository"], { cwd: repoPath });
10971
- if (!result.ok || result.stdout.trim() !== "true") return void 0;
10972
- return [SHALLOW_HISTORY_LIMITATION];
10973
- }
10974
-
10975
13157
  // src/witan/ingest.ts
10976
13158
  init_esm_shims();
10977
13159
  import { lstatSync as lstatSync3, opendirSync, readFileSync as readFileSync4, realpathSync as realpathSync3 } from "fs";
@@ -10999,54 +13181,7 @@ function clampFinding(finding) {
10999
13181
  // src/witan/generic-adapter.ts
11000
13182
  init_esm_shims();
11001
13183
  import { readFileSync } from "fs";
11002
-
11003
- // src/witan/ingest-files.ts
11004
- init_esm_shims();
11005
- import { lstatSync as lstatSync2, realpathSync as realpathSync2 } from "fs";
11006
- import { dirname as dirname2, isAbsolute, relative as relative2, resolve as resolve2, sep as sep2 } from "path";
11007
- var MAX_INGEST_DOCUMENT_BYTES = 8 * 1024 * 1024;
11008
- function isContainedPath(candidate, directory) {
11009
- const relativePath = relative2(directory, candidate);
11010
- return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${sep2}`) && !isAbsolute(relativePath);
11011
- }
11012
- function isMissingIngestPath(error) {
11013
- return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
11014
- }
11015
- function resolveIngestFilePath(filePath, inputDirectory = dirname2(filePath)) {
11016
- const lexicalDirectory = resolve2(inputDirectory);
11017
- const lexicalCandidate = resolve2(filePath);
11018
- const directoryStat = lstatSync2(lexicalDirectory);
11019
- if (directoryStat.isSymbolicLink()) {
11020
- throw new Error(`Cejel: refusing symlinked ingest directory: ${inputDirectory}`);
11021
- }
11022
- if (!directoryStat.isDirectory()) {
11023
- throw new Error(`Cejel: refusing non-directory ingest boundary: ${inputDirectory}`);
11024
- }
11025
- const candidateStat = lstatSync2(lexicalCandidate);
11026
- if (candidateStat.isSymbolicLink()) {
11027
- throw new Error(`Cejel: refusing symlinked ingest file: ${filePath}`);
11028
- }
11029
- if (!candidateStat.isFile()) {
11030
- throw new Error(`Cejel: refusing non-regular ingest file: ${filePath}`);
11031
- }
11032
- if (candidateStat.size > MAX_INGEST_DOCUMENT_BYTES) {
11033
- throw new Error(
11034
- `Cejel: ingest file exceeds the ${MAX_INGEST_DOCUMENT_BYTES.toLocaleString("en-US")} byte budget: ${filePath}`
11035
- );
11036
- }
11037
- const resolvedDirectory = realpathSync2(lexicalDirectory);
11038
- const resolvedCandidate = realpathSync2(lexicalCandidate);
11039
- if (!isContainedPath(resolvedCandidate, resolvedDirectory)) {
11040
- throw new Error(`Cejel: refusing ingest file outside its resolved input directory: ${filePath}`);
11041
- }
11042
- return resolvedCandidate;
11043
- }
11044
- function isResolvedIngestPathContained(candidate, directory) {
11045
- return isContainedPath(candidate, directory);
11046
- }
11047
-
11048
- // src/witan/generic-adapter.ts
11049
- var CEJEL_GENERIC_INGEST_CONTRACT_VERSION = "1.0";
13184
+ var CEJEL_GENERIC_INGEST_CONTRACT_VERSION = "1.1";
11050
13185
  var CEJEL_GENERIC_INGEST_CONTRACT_MAJOR = 1;
11051
13186
  var GenericFindingV1Schema = external_exports.object({
11052
13187
  ruleId: external_exports.string().min(1),
@@ -11064,6 +13199,17 @@ var GenericSignalDocumentV1Schema = external_exports.object({
11064
13199
  tool: external_exports.string().trim().min(1),
11065
13200
  signals: external_exports.array(GenericSignalV1Schema)
11066
13201
  }).passthrough();
13202
+ var GenericIdentityV1_1Schema = external_exports.object({
13203
+ subject: external_exports.object({
13204
+ repository: external_exports.string().trim().min(1).max(2048).optional(),
13205
+ commit: external_exports.string().regex(/^[0-9a-fA-F]{7,64}$/, "expected a 7-64 character hexadecimal commit id").optional()
13206
+ }).passthrough().optional(),
13207
+ producer: external_exports.object({
13208
+ name: external_exports.string().trim().min(1).max(200).optional(),
13209
+ version: external_exports.string().trim().min(1).max(200).optional()
13210
+ }).passthrough().optional(),
13211
+ observedAt: external_exports.string().datetime({ offset: true }).optional()
13212
+ }).passthrough();
11067
13213
  function isGenericSignalDocument(raw) {
11068
13214
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return false;
11069
13215
  const doc = raw;
@@ -11091,8 +13237,10 @@ function assertSupportedContractVersion(version) {
11091
13237
  function formatValidationIssues(error) {
11092
13238
  return error.issues.map((issue) => `${issue.path.length > 0 ? issue.path.join(".") : "<document>"}: ${issue.message}`).join("; ");
11093
13239
  }
11094
- function parseGenericJson(raw) {
11095
- if (!isGenericSignalDocument(raw)) return [];
13240
+ function parseGenericDocument(raw) {
13241
+ if (!isGenericSignalDocument(raw)) {
13242
+ return { signals: [], identity: { producers: [], subjects: [] } };
13243
+ }
11096
13244
  assertSupportedContractVersion(raw.version);
11097
13245
  const parsed = GenericSignalDocumentV1Schema.safeParse(raw);
11098
13246
  if (!parsed.success) {
@@ -11100,7 +13248,23 @@ function parseGenericJson(raw) {
11100
13248
  `Cejel: invalid generic ingest contract v${raw.version}: ${formatValidationIssues(parsed.error)}`
11101
13249
  );
11102
13250
  }
11103
- return parsed.data.signals.map((signal) => ({
13251
+ const minor = Number(/^\d+\.(\d+)$/.exec(parsed.data.version)?.[1] ?? "0");
13252
+ let identity = genericIdentity({ tool: parsed.data.tool });
13253
+ if (minor >= 1) {
13254
+ const identityFields = GenericIdentityV1_1Schema.safeParse(raw);
13255
+ if (!identityFields.success) {
13256
+ throw new Error(
13257
+ `Cejel: invalid generic ingest contract v${parsed.data.version}: ${formatValidationIssues(identityFields.error)}`
13258
+ );
13259
+ }
13260
+ identity = genericIdentity({
13261
+ tool: parsed.data.tool,
13262
+ ...identityFields.data.subject ? { subject: identityFields.data.subject } : {},
13263
+ ...identityFields.data.producer ? { producer: identityFields.data.producer } : {},
13264
+ ...identityFields.data.observedAt ? { observedAt: identityFields.data.observedAt } : {}
13265
+ });
13266
+ }
13267
+ const signals = parsed.data.signals.map((signal) => ({
11104
13268
  source: parsed.data.tool,
11105
13269
  dimension: signal.dimension,
11106
13270
  weight: signal.weight,
@@ -11111,6 +13275,7 @@ function parseGenericJson(raw) {
11111
13275
  ...finding.location ? { location: finding.location } : {}
11112
13276
  }))
11113
13277
  }));
13278
+ return { signals, identity };
11114
13279
  }
11115
13280
 
11116
13281
  // src/witan/sarif-adapter.ts
@@ -11377,11 +13542,13 @@ function sanitizeIngestSource(source) {
11377
13542
  maxLength: MAX_INGEST_SOURCE_LENGTH
11378
13543
  });
11379
13544
  }
11380
- function parseIngestFile(filePath, options = {}) {
13545
+ function readIngestDocument(filePath, options = {}) {
11381
13546
  const resolvedFilePath = resolveIngestFilePath(filePath);
13547
+ const bytes = readFileSync4(resolvedFilePath);
13548
+ const sha256 = sha256OfBytes(bytes);
11382
13549
  let raw;
11383
13550
  try {
11384
- raw = JSON.parse(stripBom(readFileSync4(resolvedFilePath, "utf8")));
13551
+ raw = JSON.parse(stripBom(bytes.toString("utf8")));
11385
13552
  } catch (error) {
11386
13553
  const message = error instanceof Error ? error.message : String(error);
11387
13554
  throw new Error(`Cejel: could not parse ingest file as JSON: ${filePath} (${message})`);
@@ -11393,10 +13560,23 @@ function parseIngestFile(filePath, options = {}) {
11393
13560
  );
11394
13561
  }
11395
13562
  let signals;
11396
- if (looksLikeSarif(raw)) signals = parseSarifJson(raw, options.extraSarifDimensionRules);
11397
- else if (looksLikeScorecard(raw)) signals = parseScorecardJson(raw);
11398
- else if (isGenericSignalDocument(raw)) signals = parseGenericJson(raw);
11399
- if (!signals) {
13563
+ let format;
13564
+ let identity = { producers: [], subjects: [] };
13565
+ if (looksLikeSarif(raw)) {
13566
+ signals = parseSarifJson(raw, options.extraSarifDimensionRules);
13567
+ format = "sarif";
13568
+ identity = extractSarifIdentity(raw);
13569
+ } else if (looksLikeScorecard(raw)) {
13570
+ signals = parseScorecardJson(raw);
13571
+ format = "scorecard";
13572
+ identity = extractScorecardIdentity(raw);
13573
+ } else if (isGenericSignalDocument(raw)) {
13574
+ const generic = parseGenericDocument(raw);
13575
+ signals = generic.signals;
13576
+ format = "generic";
13577
+ identity = generic.identity;
13578
+ }
13579
+ if (!signals || !format) {
11400
13580
  throw new Error(
11401
13581
  `Cejel: unrecognized ingest file format: ${filePath} \u2014 expected SARIF (a "runs" array), OpenSSF Scorecard JSON (a "checks" array), or the versioned generic Cejel external-signal shape (a "version" string + "tool" string + "signals" array). See the cejel README "Aggregate your scanners".`
11402
13582
  );
@@ -11408,12 +13588,17 @@ function parseIngestFile(filePath, options = {}) {
11408
13588
  `
11409
13589
  );
11410
13590
  }
11411
- return signals.map((signal) => ({
11412
- ...signal,
11413
- source: sanitizeIngestSource(signal.source),
11414
- provenance: options.provenance ?? "operator_supplied",
11415
- findings: signal.findings.map(clampFinding)
11416
- }));
13591
+ return {
13592
+ signals: signals.map((signal) => ({
13593
+ ...signal,
13594
+ source: sanitizeIngestSource(signal.source),
13595
+ provenance: options.provenance ?? "operator_supplied",
13596
+ findings: signal.findings.map(clampFinding)
13597
+ })),
13598
+ sha256,
13599
+ format,
13600
+ identity
13601
+ };
11417
13602
  }
11418
13603
  function expandIngestPattern(pattern, maxMatches = Number.POSITIVE_INFINITY) {
11419
13604
  if (!pattern.includes("*")) return [pattern];
@@ -11497,6 +13682,7 @@ function discoverIngestInputs(repoPath, maxMatches = Number.POSITIVE_INFINITY) {
11497
13682
  // src/witan/public-scan.ts
11498
13683
  var MAX_INGEST_DOCUMENTS = 128;
11499
13684
  var MAX_RETAINED_INGEST_FINDINGS = 1e4;
13685
+ var MAX_INPUT_BINDING_RECORDS = 2 * MAX_INGEST_DOCUMENTS;
11500
13686
  function scoreRepoWithPublicCejel(options) {
11501
13687
  if (options.rubricVersion !== void 0) {
11502
13688
  assertSelectableRubricVersion(options.rubricVersion);
@@ -11508,35 +13694,90 @@ function scoreRepoWithPublicCejel(options) {
11508
13694
  ...options.generatedAt ? { generatedAt: options.generatedAt } : {},
11509
13695
  ...options.rubricVersion ? { rubricVersion: options.rubricVersion } : {}
11510
13696
  });
11511
- const inputSignals = resolvePublicIngestSignals(options);
13697
+ const scannedSubject = readScannedSubject(
13698
+ options.repoPath,
13699
+ input.repo.headSha,
13700
+ options.subjectRepository
13701
+ );
13702
+ const { signals: inputSignals, documents } = resolvePublicIngestInputs(options);
13703
+ const inputBinding = documents.length > 0 ? buildInputBinding(documents, scannedSubject, options.strictIngest ? "strict" : "record-only") : void 0;
13704
+ if (options.strictIngest && inputBinding) assertStrictIngestBinding(documents, inputBinding);
11512
13705
  const scored = createWitanReport(
11513
13706
  input,
11514
13707
  inputSignals.length > 0 ? inputSignals : void 0,
11515
13708
  void 0,
11516
13709
  options.toolVersion
11517
13710
  );
11518
- const historyLimitations = detectHistoryLimitations(options.repoPath);
11519
- const report = historyLimitations ? { ...scored, historyLimitations } : scored;
13711
+ const historyLimitations = detectHistoryLimitations(
13712
+ options.repoPath,
13713
+ usesV27FalseAssuranceClosure(scored.rubricVersion)
13714
+ );
13715
+ const withHistory = historyLimitations ? { ...scored, historyLimitations } : scored;
13716
+ const report = inputBinding ? { ...withHistory, inputBinding } : withHistory;
11520
13717
  if (report.verdict !== "insufficient_source" || !isWitanNoMeasurementAbstention(report)) {
11521
- return report;
13718
+ return assertReportVerifiable(report);
11522
13719
  }
11523
13720
  const contextualReason = explainNoMeasurementSourceCoverage(
11524
13721
  options.repoPath,
11525
13722
  report.rubricVersion
11526
13723
  );
11527
- return contextualReason ? {
11528
- ...report,
11529
- insufficientSourceReason: contextualReason
11530
- } : report;
13724
+ return assertReportVerifiable(
13725
+ contextualReason ? {
13726
+ ...report,
13727
+ insufficientSourceReason: contextualReason
13728
+ } : report
13729
+ );
13730
+ }
13731
+ function assertReportVerifiable(report) {
13732
+ const result = WitanReportSchema.safeParse(report);
13733
+ if (result.success) return report;
13734
+ const issues = result.error.issues.slice(0, 8).map((issue) => ` - ${issue.path.join(".") || "<root>"}: ${issue.message}`);
13735
+ throw new Error(
13736
+ [
13737
+ "Cejel: the assembled report does not satisfy the report schema `cejel verify` applies; no certificate was written.",
13738
+ ...issues,
13739
+ ...result.error.issues.length > issues.length ? [` ...and ${result.error.issues.length - issues.length} more.`] : []
13740
+ ].join("\n")
13741
+ );
13742
+ }
13743
+ function readScannedSubject(repoPath, headSha, subjectRepository) {
13744
+ let repository;
13745
+ if (subjectRepository !== void 0) {
13746
+ repository = normalizeRepositoryIdentity(subjectRepository);
13747
+ if (repository === void 0) {
13748
+ throw new Error(
13749
+ `Cejel: --subject-repository value (${subjectRepository.length} characters, not echoed) is not a repository identity Cejel can compare. Expected a form such as github.com/owner/repo or https://github.com/owner/repo.git, with a DNS host name and a path.`
13750
+ );
13751
+ }
13752
+ }
13753
+ let commitTime;
13754
+ if (headSha) {
13755
+ const result = execGit(["show", "-s", "--format=%ct", "HEAD"], { cwd: repoPath });
13756
+ const seconds = result.ok ? Number(result.stdout.trim()) : Number.NaN;
13757
+ if (Number.isSafeInteger(seconds) && seconds >= 0) {
13758
+ commitTime = plainIsoInstant(seconds * 1e3);
13759
+ }
13760
+ }
13761
+ return {
13762
+ ...repository !== void 0 ? { repository } : {},
13763
+ ...headSha ? { commit: headSha.toLowerCase() } : {},
13764
+ ...commitTime !== void 0 ? { commitTime } : {}
13765
+ };
11531
13766
  }
11532
- function resolvePublicIngestSignals(options) {
13767
+ function resolvePublicIngestInputs(options) {
11533
13768
  const seen = /* @__PURE__ */ new Set();
11534
13769
  const documents = [];
13770
+ const samePathRepeats = /* @__PURE__ */ new Map();
11535
13771
  const addDocument = (file, provenance) => {
11536
13772
  const resolved = resolveIngestFilePath(file);
11537
- if (seen.has(resolved)) return;
13773
+ if (seen.has(resolved)) {
13774
+ if (!samePathRepeats.has(resolved)) {
13775
+ samePathRepeats.set(resolved, { file: resolved, displayPath: file, provenance });
13776
+ }
13777
+ return;
13778
+ }
11538
13779
  seen.add(resolved);
11539
- documents.push({ file: resolved, provenance });
13780
+ documents.push({ file: resolved, displayPath: file, provenance });
11540
13781
  if (documents.length > MAX_INGEST_DOCUMENTS) {
11541
13782
  throw new Error(
11542
13783
  `Cejel: ${documents.length} unique ingest documents exceed the ${MAX_INGEST_DOCUMENTS} ingest document budget; refusing to produce a partial certificate.`
@@ -11562,12 +13803,16 @@ function resolvePublicIngestSignals(options) {
11562
13803
  }
11563
13804
  }
11564
13805
  const signals = [];
13806
+ const records = [];
13807
+ const readByPath = /* @__PURE__ */ new Map();
13808
+ const scoredDigests = /* @__PURE__ */ new Set();
11565
13809
  let retainedFindings = 0;
11566
13810
  for (const document of documents) {
11567
- const parsed = parseIngestFile(document.file, {
13811
+ const read = readIngestDocument(document.file, {
11568
13812
  maxFindingCandidates: MAX_RETAINED_INGEST_FINDINGS - retainedFindings,
11569
13813
  provenance: document.provenance
11570
13814
  });
13815
+ const parsed = read.signals;
11571
13816
  const documentFindings = parsed.reduce((count, signal) => count + signal.findings.length, 0);
11572
13817
  retainedFindings += documentFindings;
11573
13818
  if (retainedFindings > MAX_RETAINED_INGEST_FINDINGS) {
@@ -11576,8 +13821,30 @@ function resolvePublicIngestSignals(options) {
11576
13821
  );
11577
13822
  }
11578
13823
  signals.push(...parsed);
13824
+ readByPath.set(document.file, read);
13825
+ records.push({
13826
+ sha256: read.sha256,
13827
+ format: read.format,
13828
+ provenance: document.provenance,
13829
+ identity: read.identity,
13830
+ displayPath: document.displayPath,
13831
+ ...scoredDigests.has(read.sha256) ? { duplicate: "same-content" } : {}
13832
+ });
13833
+ scoredDigests.add(read.sha256);
13834
+ }
13835
+ for (const repeat of samePathRepeats.values()) {
13836
+ const read = readByPath.get(repeat.file);
13837
+ if (!read) continue;
13838
+ records.push({
13839
+ sha256: read.sha256,
13840
+ format: read.format,
13841
+ provenance: repeat.provenance,
13842
+ identity: read.identity,
13843
+ displayPath: repeat.displayPath,
13844
+ duplicate: "same-path"
13845
+ });
11579
13846
  }
11580
- return signals;
13847
+ return { signals, documents: records };
11581
13848
  }
11582
13849
 
11583
13850
  // src/product-identity.ts
@@ -11776,6 +14043,8 @@ function runCejelScan(options) {
11776
14043
  // are third-party repositories and must opt in explicitly.
11777
14044
  autoDiscoverIngest: true,
11778
14045
  warnOnEmptyIngestMatch: options.warnOnEmptyIngestMatch,
14046
+ ...options.strictIngest ? { strictIngest: true } : {},
14047
+ ...options.subjectRepository ? { subjectRepository: options.subjectRepository } : {},
11779
14048
  ...options.toolVersion ? { toolVersion: options.toolVersion } : {}
11780
14049
  });
11781
14050
  const summary = buildWitanCliSummary(report);
@@ -11786,7 +14055,13 @@ export {
11786
14055
  WitanReportSchema,
11787
14056
  computeApplicableMeasuredCoverage,
11788
14057
  formatCoverageSummary,
14058
+ EXTERNAL_FINDINGS_DISPLAY_LIMIT,
11789
14059
  formatExternalSourceLine,
14060
+ inheritedFindingNote,
14061
+ importedAdjustmentSentence,
14062
+ inputBindingHeadline,
14063
+ describeInputBindingRecord,
14064
+ describeScannedSubject,
11790
14065
  WITAN_RUBRIC_VERSION_V22,
11791
14066
  assertSelectableRubricVersion,
11792
14067
  isProspectiveRubricVersion,
@@ -11806,6 +14081,9 @@ export {
11806
14081
  serializeWitanReport,
11807
14082
  createWitanAttestation,
11808
14083
  verifyWitanAttestationBinding,
14084
+ stripTerminalControls,
14085
+ verifyRecordedInputDigests,
14086
+ resolveIngestFilePath,
11809
14087
  deriveProductIdentity,
11810
14088
  runCejelScan
11811
14089
  };