@cejel/cejel 0.6.6 → 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,
@@ -572,6 +651,9 @@ var WITAN_DIRECTORY_WALK_AUTHENTICATED_A1_ABSENCE_SUMMARY = "Reviewable source i
572
651
  var WITAN_LEGACY_AUTHENTICATED_A1_ABSENCE_SUMMARY = "Reviewable source is present, but the complete tracked inventory contains no concrete test file, configured test runner, CI test command, or coverage configuration.";
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.";
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: ";
575
657
  function isWitanNoMeasurementAbstention(report) {
576
658
  return report.verdict === "insufficient_source" && report.insufficientSourceReason.startsWith(WITAN_NO_MEASUREMENT_REASON) && report.criteria.every(
577
659
  ({ status }) => status === "not_applicable" || status === "insufficient_data"
@@ -635,6 +717,76 @@ function collectExternalFindings(consumedSignals) {
635
717
  });
636
718
  }
637
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
+
638
790
  // src/witan/finding-presentation.ts
639
791
  init_esm_shims();
640
792
 
@@ -662,7 +814,11 @@ var WITAN_RUBRIC_BEHAVIOUR_FINGERPRINTS = Object.freeze({
662
814
  // fires on it and, by amendment 1, A5 is scored without reconciliation_artifact_depth. The A5
663
815
  // component and the report components of the specimens whose code-trust score that moves differ
664
816
  // from v25's. A2 does not move on this corpus.
665
- [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"
666
822
  });
667
823
  function rubricBehaviourFingerprint(rubricVersion) {
668
824
  return WITAN_RUBRIC_BEHAVIOUR_FINGERPRINTS[rubricVersion];
@@ -730,7 +886,7 @@ var WITAN_RUBRIC = [
730
886
 
731
887
  // src/witan/scoring.ts
732
888
  function usesV17DetectorClosure(rubricVersion) {
733
- 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;
734
890
  }
735
891
  var MAX_SIGNAL_ADJUSTMENT = 0.8;
736
892
  function createWitanReport(input, inputSignals, rubric = WITAN_RUBRIC, toolVersion) {
@@ -764,7 +920,8 @@ function createWitanReport(input, inputSignals, rubric = WITAN_RUBRIC, toolVersi
764
920
  evidence: scored.evidence,
765
921
  findings: scored.findings,
766
922
  metrics: scored.metrics,
767
- ...signal?.notes ? { notes: signal.notes } : {}
923
+ ...signal?.notes ? { notes: signal.notes } : {},
924
+ ...signal?.disclosures ? { disclosures: signal.disclosures } : {}
768
925
  };
769
926
  }
770
927
  const { adjustedScore, adjustment, summaries } = applySignalAdjustment(
@@ -786,11 +943,16 @@ function createWitanReport(input, inputSignals, rubric = WITAN_RUBRIC, toolVersi
786
943
  category: rubricCriterion.category,
787
944
  score: finalScore,
788
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 } : {},
789
950
  status: finalStatus,
790
951
  evidence: scored.evidence,
791
952
  findings: scored.findings,
792
953
  metrics: scored.metrics,
793
- ...signal?.notes ? { notes: signal.notes } : {}
954
+ ...signal?.notes ? { notes: signal.notes } : {},
955
+ ...signal?.disclosures ? { disclosures: signal.disclosures } : {}
794
956
  };
795
957
  });
796
958
  const categoryOrder = [];
@@ -901,7 +1063,7 @@ function statusForScore(score) {
901
1063
  }
902
1064
  function statusAfterInputAdjustment(nativeStatus, nativeScore, adjustment, rubricVersion) {
903
1065
  if (adjustment === 0) return nativeStatus;
904
- 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) {
905
1067
  return statusForScore(roundScore(Math.max(0, nativeScore - adjustment)));
906
1068
  }
907
1069
  const statusFloor = {
@@ -949,6 +1111,7 @@ function mergeSignalsByCriterion(signals) {
949
1111
  findings: [...existing.findings, ...signal.findings],
950
1112
  metrics: [...existing.metrics ?? [], ...signal.metrics ?? []],
951
1113
  notes: signal.notes ?? existing.notes,
1114
+ ...mergedDisclosures(existing.disclosures, signal.disclosures),
952
1115
  ...existingAbstains || signalAbstains ? {
953
1116
  insufficientData: true,
954
1117
  ...abstentionReason !== void 0 ? { insufficientDataReason: abstentionReason } : {}
@@ -957,8 +1120,12 @@ function mergeSignalsByCriterion(signals) {
957
1120
  }
958
1121
  return merged;
959
1122
  }
1123
+ function mergedDisclosures(existing, incoming) {
1124
+ const disclosures = [.../* @__PURE__ */ new Set([...existing ?? [], ...incoming ?? []])].slice(0, 4);
1125
+ return disclosures.length > 0 ? { disclosures } : {};
1126
+ }
960
1127
  function usesMetricScoring(rubricVersion) {
961
- 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;
962
1129
  }
963
1130
  function unmeasuredStatus(rubricVersion) {
964
1131
  return rubricVersion === WITAN_TRADING_RUBRIC_VERSION_V0 ? "unverified" : "insufficient_data";
@@ -1346,7 +1513,7 @@ function calibratedCriterionStateV11(criterionId, metrics) {
1346
1513
  return void 0;
1347
1514
  }
1348
1515
  function ensureFindingsExplainStatus(criterionId, score, status, findings, evidence, rubricVersion) {
1349
- 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) {
1350
1517
  return [...findings];
1351
1518
  }
1352
1519
  if (status !== "critical" && status !== "warning") return [...findings];
@@ -1856,6 +2023,12 @@ function buildNextSteps(report, unmeasured, notApplicable, a1, coverageMetric) {
1856
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.`
1857
2024
  );
1858
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
+ }
1859
2032
  if (steps.length === 0) {
1860
2033
  steps.push(
1861
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."
@@ -1942,7 +2115,9 @@ var WITHHELD_PATH_REASON_LABELS = {
1942
2115
  non_regular_file: "not a regular file",
1943
2116
  unreadable: "unreadable",
1944
2117
  denied_path: "denied path",
1945
- 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"
1946
2121
  };
1947
2122
  function describeWithheldReasons(entries) {
1948
2123
  return [...new Set(entries.map((entry) => WITHHELD_PATH_REASON_LABELS[entry.reason]))].sort().join(", ");
@@ -1973,6 +2148,7 @@ function buildWithheldPathsGapSentence(report) {
1973
2148
  }
1974
2149
  var SETTLING_EVIDENCE_DISCLOSURES = [
1975
2150
  { criterionId: "A2", sentence: WITAN_A2_TENANT_ISOLATION_UNSETTLED_DISCLOSURE },
2151
+ { criterionId: "A3", sentence: WITAN_A3_BUILD_STEP_UNSETTLED_DISCLOSURE },
1976
2152
  { criterionId: "A5", sentence: WITAN_A5_RECONCILIATION_UNSETTLED_DISCLOSURE }
1977
2153
  ];
1978
2154
  function settlingEvidenceDisclosures(report) {
@@ -1982,6 +2158,18 @@ function settlingEvidenceDisclosures(report) {
1982
2158
  )
1983
2159
  ).map(({ sentence }) => sentence);
1984
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
+ }
1985
2173
  function describeEstablishedDimensions(report, measured, applicable) {
1986
2174
  const total = report.criteria.length;
1987
2175
  const counts = formatMeasuredDimensionCounts(computeMeasuredCoverage(report));
@@ -2034,6 +2222,12 @@ function buildRelyingPartySummary(report) {
2034
2222
  `${contentEntries(actionableSkips, "was", "were")} skipped for the reasons itemized in the certificate.`
2035
2223
  );
2036
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
+ }
2037
2231
  const excludedByExtension = report.contentReadSummary?.byReason.excludedByExtension ?? 0;
2038
2232
  if (excludedByExtension > 0) {
2039
2233
  gaps.push(
@@ -2041,6 +2235,8 @@ function buildRelyingPartySummary(report) {
2041
2235
  );
2042
2236
  }
2043
2237
  gaps.push(...settlingEvidenceDisclosures(report));
2238
+ gaps.push(...placeholderSetAsideDisclosures(report));
2239
+ gaps.push(...unshownCheckWorkflowDisclosures(report));
2044
2240
  const withheldPathsSentence = buildWithheldPathsGapSentence(report);
2045
2241
  if (withheldPathsSentence !== void 0) {
2046
2242
  gaps.push(withheldPathsSentence);
@@ -2184,7 +2380,7 @@ function renderWitanHtmlReport(report, options = {}) {
2184
2380
  <aside class="score-panel" aria-label="Summary trust score">
2185
2381
  <div class="score-badge">Summary</div>
2186
2382
  ${report.verdict === "insufficient_source" ? `<div class="verdict">${escapeHtml(renderWitanAbstentionLabel(report))}</div>
2187
- <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>
2188
2384
  <div class="score-unit">/ 4.0 overall</div>
2189
2385
  <div class="verdict">${escapeHtml(renderVerdict(report.overallScore))}</div>
2190
2386
  <div class="subscores">
@@ -2241,6 +2437,8 @@ function renderWitanHtmlReport(report, options = {}) {
2241
2437
  </article>
2242
2438
  </section>` : ""}
2243
2439
 
2440
+ ${renderInputBindingSection(report)}
2441
+
2244
2442
  ${renderGlossary(glossaryEntriesForReport(report))}
2245
2443
  </main>
2246
2444
  </body>
@@ -2305,7 +2503,8 @@ function renderContentReadSummary(report) {
2305
2503
  <li>Too large: ${summary.byReason.tooLarge}</li>
2306
2504
  <li>Excluded by extension: ${summary.byReason.excludedByExtension}</li>
2307
2505
  <li>Denied path: ${summary.byReason.deniedPath}</li>
2308
- <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>` : ""}
2309
2508
  </ul>
2310
2509
  ${affected}
2311
2510
  </section>`;
@@ -2340,7 +2539,7 @@ function formatSourceProvenanceLine(summary) {
2340
2539
  function renderExternalFindingItems(findings) {
2341
2540
  const shown = findings.slice(0, EXTERNAL_FINDINGS_DISPLAY_LIMIT);
2342
2541
  const items = shown.map(
2343
- (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>`
2344
2543
  );
2345
2544
  const remaining = findings.length - shown.length;
2346
2545
  if (remaining > 0) {
@@ -2348,6 +2547,21 @@ function renderExternalFindingItems(findings) {
2348
2547
  }
2349
2548
  return items;
2350
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
+ }
2351
2565
  function criteriaByCategory(report, category) {
2352
2566
  return report.criteria.filter((criterion) => criterion.category === category);
2353
2567
  }
@@ -2391,8 +2605,8 @@ function renderCriterionCard(criterion, gitHistoryUnavailable) {
2391
2605
  </div>
2392
2606
  ${renderStatusChip(criterion.id, criterion.status)}
2393
2607
  </div>
2394
- <div class="criterion-score">${criterion.status === "not_applicable" ? "N/A" : criterion.status === "insufficient_data" ? "No data" : formatScore(criterion.score)}</div>
2395
- ${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)}
2396
2610
  ${historyWarning}
2397
2611
  <ul class="criterion-metrics">
2398
2612
  ${metrics.length > 0 ? metrics.map((item) => `<li>${item}</li>`).join("") : "<li>No measured depth metrics supplied.</li>"}
@@ -2402,6 +2616,11 @@ function renderCriterionCard(criterion, gitHistoryUnavailable) {
2402
2616
  </ul>
2403
2617
  </article>`;
2404
2618
  }
2619
+ function renderImportedAdjustment(criterion) {
2620
+ const sentence = importedAdjustmentSentence(criterion);
2621
+ return sentence ? `
2622
+ <p class="status-explanation">${escapeHtml(sentence)}</p>` : "";
2623
+ }
2405
2624
  function renderStatusReconciliation(criterion) {
2406
2625
  if (criterion.status === "not_applicable") {
2407
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>';
@@ -2414,14 +2633,14 @@ function renderStatusReconciliation(criterion) {
2414
2633
  }
2415
2634
  const numericBand = scoreBandForPresentation(criterion.score);
2416
2635
  if (numericBand === criterion.status) {
2417
- 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>`;
2418
2637
  }
2419
2638
  if (criterion.status !== "verified" && criterion.status !== "info" && criterion.status !== "warning" && criterion.status !== "critical") {
2420
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>`;
2421
2640
  }
2422
2641
  const entry = CERTIFICATE_GLOSSARY.find((candidate) => candidate.key === "labels");
2423
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>";
2424
- 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>`;
2425
2644
  }
2426
2645
  function scoreBandForPresentation(score) {
2427
2646
  if (score >= 3.5) return "verified";
@@ -2447,7 +2666,7 @@ function renderMetric(criterion, metric2) {
2447
2666
  }
2448
2667
  function renderCategoryScore(score, coverage, category) {
2449
2668
  const categoryCoverage = coverage.byCategory.find((entry) => entry.category === category);
2450
- 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);
2451
2670
  }
2452
2671
  function renderStatusChip(criterionId, status) {
2453
2672
  const entry = CERTIFICATE_GLOSSARY.find((candidate) => candidate.key === "labels");
@@ -2513,7 +2732,7 @@ function renderMachineVerdict(verdict) {
2513
2732
  return "Insufficient source";
2514
2733
  }
2515
2734
  }
2516
- function formatScore(score) {
2735
+ function formatScore2(score) {
2517
2736
  return score.toFixed(1);
2518
2737
  }
2519
2738
  function formatDate(value) {
@@ -2704,7 +2923,7 @@ function badgeMessage(report, verdict) {
2704
2923
  if ((report.scanLimitations?.length ?? 0) > 0) return "unrated: limited evidence";
2705
2924
  if (isWitanNoMeasurementAbstention(report)) return "unrated: no measurable evidence";
2706
2925
  if (report.verdict === "insufficient_source") return "unrated: no source";
2707
- return `${formatScore2(report.overallScore)}/4.0 ${verdict.toLowerCase()}`;
2926
+ return `${formatScore3(report.overallScore)}/4.0 ${verdict.toLowerCase()}`;
2708
2927
  }
2709
2928
  function renderWitanBadgeEndpoint(report) {
2710
2929
  const verdict = renderReportVerdict(report);
@@ -2748,7 +2967,7 @@ function renderWitanBadgeSvg(report) {
2748
2967
  </svg>
2749
2968
  `;
2750
2969
  }
2751
- function formatScore2(score) {
2970
+ function formatScore3(score) {
2752
2971
  return score.toFixed(1);
2753
2972
  }
2754
2973
  function escapeXml(value) {
@@ -2885,12 +3104,6 @@ function verifyWitanAttestationBinding(statement, report, options = {}) {
2885
3104
  return { valid: errors.length === 0, errors };
2886
3105
  }
2887
3106
 
2888
- // src/witan/index.ts
2889
- init_esm_shims();
2890
-
2891
- // src/witan/markdown.ts
2892
- init_esm_shims();
2893
-
2894
3107
  // src/presentation-safety.ts
2895
3108
  init_esm_shims();
2896
3109
  var PRESENTATION_CONTROL_PATTERN = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu;
@@ -2907,17 +3120,530 @@ function sanitizePresentationLine(value, options) {
2907
3120
  }
2908
3121
  return `${prefix}${ellipsis}`;
2909
3122
  }
3123
+ function stripTerminalControls(text) {
3124
+ return text.replace(/[\t\n]/g, " ").replace(/[\u0000-\u001f\u007f-\u009f]/g, "");
3125
+ }
2910
3126
 
2911
- // src/witan/repo-signals.ts
3127
+ // src/witan/input-binding.ts
2912
3128
  init_esm_shims();
2913
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";
2914
3627
  import {
2915
- lstatSync,
3628
+ lstatSync as lstatSync2,
2916
3629
  readdirSync,
2917
- realpathSync,
3630
+ realpathSync as realpathSync2,
2918
3631
  statSync
2919
3632
  } from "fs";
2920
- 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
+ }
2921
3647
 
2922
3648
  // src/witan/json-safe.ts
2923
3649
  init_esm_shims();
@@ -2927,7 +3653,10 @@ function stripBom(text) {
2927
3653
 
2928
3654
  // src/witan/repo-signals.ts
2929
3655
  function usesV17DetectorClosure2(rubricVersion) {
2930
- 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;
2931
3660
  }
2932
3661
  var LEGACY_INVENTORY_ABSENCE_CONTEXT = {
2933
3662
  rubricGeneration: "legacy",
@@ -3027,9 +3756,16 @@ function wholesaleCriterionAbstention(signal, reasons) {
3027
3756
  metrics: [],
3028
3757
  insufficientData: true,
3029
3758
  ...insufficientDataReason !== void 0 ? { insufficientDataReason } : {},
3030
- 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."
3031
3763
  };
3032
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
+ }
3033
3769
  function skipReasonsForCriterion(criterionId, tracked) {
3034
3770
  const combined = new Set(tracked.criterionSkipReasons.get(criterionId) ?? []);
3035
3771
  const prefix = contentReadSignalKey(criterionId, "");
@@ -3044,15 +3780,24 @@ function skipReasonsForSignal(criterionId, signalName, tracked) {
3044
3780
  return reasons ? new Set(reasons) : void 0;
3045
3781
  }
3046
3782
  function buildWitanInputFromRepo(options) {
3047
- const tracked = trackContentReads(options.repoPath, () => buildWitanInputFromRepoUntracked(options));
3048
- const affectedCriteria = tracked.affectedCriteria;
3049
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;
3050
3790
  if (rubricVersion !== WITAN_RUBRIC_VERSION_V23) {
3051
3791
  const readFailureCriteria = [];
3052
3792
  const coverageLimitedCriteria = [];
3793
+ const lfsKept = /* @__PURE__ */ new Set();
3053
3794
  const signals2 = (tracked.value.signals ?? []).map((signal) => {
3054
3795
  if (!affectedCriteria.has(signal.criterionId)) return signal;
3055
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
+ }
3056
3801
  const { insufficientDataReason } = classifyAbstentionReason(reasons);
3057
3802
  (insufficientDataReason !== void 0 ? coverageLimitedCriteria : readFailureCriteria).push(
3058
3803
  signal.criterionId
@@ -3074,7 +3819,10 @@ function buildWitanInputFromRepo(options) {
3074
3819
  ...tracked.value,
3075
3820
  signals: signals2,
3076
3821
  ...scanLimitations2.length > 0 ? { scanLimitations: scanLimitations2 } : {},
3077
- contentReadSummary: tracked.summary
3822
+ contentReadSummary: lfsKept.size > 0 ? {
3823
+ ...tracked.summary,
3824
+ affectedCriteria: tracked.summary.affectedCriteria.filter((id) => !lfsKept.has(id))
3825
+ } : tracked.summary
3078
3826
  };
3079
3827
  }
3080
3828
  const wholesaleAbstainedReadFailureCriteria = [];
@@ -3205,6 +3953,8 @@ function buildWitanInputFromRepoUntracked(options) {
3205
3953
  const usesV24SecretContentContext = rubricVersion === WITAN_RUBRIC_VERSION_V24;
3206
3954
  const usesV25InlineHealthRoute = rubricVersion === WITAN_RUBRIC_VERSION_V25 || rubricVersion === WITAN_RUBRIC_VERSION_V26;
3207
3955
  const usesV26SettlingEvidence = rubricVersion === WITAN_RUBRIC_VERSION_V26;
3956
+ const usesV26BuildStepEvidence = rubricVersion === WITAN_RUBRIC_VERSION_V26;
3957
+ const usesV27Closure = usesV27FalseAssuranceClosure(rubricVersion);
3208
3958
  const structuralArchetype = classifyRepoArchetype(inventoryFiles, rubricVersion);
3209
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(
3210
3960
  options.repoPath,
@@ -3246,6 +3996,8 @@ function buildWitanInputFromRepoUntracked(options) {
3246
3996
  usesV24SecretContentContext,
3247
3997
  usesV25InlineHealthRoute,
3248
3998
  usesV26SettlingEvidence,
3999
+ usesV26BuildStepEvidence,
4000
+ usesV27Closure,
3249
4001
  reviewableSourceProof,
3250
4002
  inventoryAbsenceContext,
3251
4003
  scanLimitations
@@ -3583,7 +4335,7 @@ function isRepresentativeSourceText(contents) {
3583
4335
  return disallowedControls / contents.length <= 0.01;
3584
4336
  }
3585
4337
  function representativeSourceRank(rankKey, file) {
3586
- return createHash3("sha256").update(`${rankKey}:${file}`).digest("hex");
4338
+ return createHash4("sha256").update(`${rankKey}:${file}`).digest("hex");
3587
4339
  }
3588
4340
  function explainNoMeasurementSourceCoverage(repoPath, rubricVersion) {
3589
4341
  if (!usesV17DetectorClosure2(rubricVersion)) return void 0;
@@ -3835,9 +4587,9 @@ function explainIgnoredScanTarget(repoPath) {
3835
4587
  }
3836
4588
  const top = topResult.stdout.trim();
3837
4589
  if (!top) return void 0;
3838
- const topResolved = realpathSync(top);
3839
- const targetResolved = realpathSync(repoPath);
3840
- 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("/");
3841
4593
  if (!target || target.startsWith("../")) return void 0;
3842
4594
  const ignored = execGit(["check-ignore", "--quiet", "--", `${target}/`], {
3843
4595
  cwd: topResolved
@@ -3860,7 +4612,7 @@ function buildNotApplicableSignal(criterionId, reason) {
3860
4612
  notApplicable: true
3861
4613
  };
3862
4614
  }
3863
- function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV18NativeRls, useV19CommitYear, useV20A3ExplicitGaps, useV21ExecutedEscalations, useV22PackageStartEntrypoint, useV23StrictCoverageAndRecipeRunners, useV23PemPrivateKeyGrammar, useV23WithheldPathAbstention, useV24SecretContentContext, useV25InlineHealthRoute, useV26SettlingEvidence, 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()) {
3864
4616
  const signals = [];
3865
4617
  const mono = resolveMonorepoContext(repoPath, scanLimitations);
3866
4618
  const collectCriterion = (criterionId, collect) => withContentReadCriterion(criterionId, collect);
@@ -3874,7 +4626,8 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
3874
4626
  useV36Detectors,
3875
4627
  useV23StrictCoverageAndRecipeRunners,
3876
4628
  reviewableSourceProof,
3877
- inventoryAbsenceContext
4629
+ inventoryAbsenceContext,
4630
+ useV27FalseAssuranceClosure
3878
4631
  )
3879
4632
  );
3880
4633
  const a2Signal = collectCriterion(
@@ -3892,7 +4645,8 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
3892
4645
  useV18NativeRls,
3893
4646
  inventoryAbsenceContext,
3894
4647
  scanLimitations,
3895
- useV26SettlingEvidence
4648
+ useV26SettlingEvidence,
4649
+ useV27FalseAssuranceClosure
3896
4650
  )
3897
4651
  );
3898
4652
  const a3Signal = collectCriterion(
@@ -3905,7 +4659,8 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
3905
4659
  useV22PackageStartEntrypoint,
3906
4660
  useV23WithheldPathAbstention,
3907
4661
  inventoryAbsenceContext,
3908
- useV25InlineHealthRoute
4662
+ useV25InlineHealthRoute,
4663
+ useV26BuildStepEvidence
3909
4664
  )
3910
4665
  );
3911
4666
  const a4Signal = collectCriterion(
@@ -3916,7 +4671,8 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
3916
4671
  mono,
3917
4672
  useV27Detectors,
3918
4673
  useV47Detectors,
3919
- inventoryAbsenceContext
4674
+ inventoryAbsenceContext,
4675
+ useV27FalseAssuranceClosure
3920
4676
  )
3921
4677
  );
3922
4678
  const a5Signal = collectCriterion(
@@ -3934,10 +4690,13 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
3934
4690
  "B1",
3935
4691
  "Repository scans do not evaluate the dispatch-trace process dimension."
3936
4692
  );
3937
- const b2Signal = collectCriterion("B2", () => collectB2PrTraceEvidence(repoPath, repoFiles));
4693
+ const b2Signal = collectCriterion(
4694
+ "B2",
4695
+ () => collectB2PrTraceEvidence(repoPath, repoFiles, useV27FalseAssuranceClosure)
4696
+ );
3938
4697
  const b3Signal = collectCriterion(
3939
4698
  "B3",
3940
- () => collectB3CiDisciplineEvidence(repoPath, repoFiles, mono)
4699
+ () => collectB3CiDisciplineEvidence(repoPath, repoFiles, mono, useV27FalseAssuranceClosure)
3941
4700
  );
3942
4701
  const b4Signal = collectCriterion(
3943
4702
  "B4",
@@ -3979,14 +4738,16 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
3979
4738
  }
3980
4739
  return signals;
3981
4740
  }
3982
- 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) {
3983
4742
  const evidence = [];
3984
4743
  const findings = [];
3985
- const allTestFiles = findConcreteTestFiles(
4744
+ const v17TestFiles = findConcreteTestFiles(
3986
4745
  repoPath,
3987
4746
  repoFiles,
3988
4747
  reviewableSourceProof ? WITAN_RUBRIC_VERSION_V17 : void 0
3989
4748
  );
4749
+ const allTestFiles = useV27FalseAssuranceClosure ? v17TestFiles.filter((file) => !PYTEST_CONFTEST_PATTERN.test(file)) : v17TestFiles;
4750
+ const runWideSkips = useV27FalseAssuranceClosure ? findPytestRunWideSkips(repoPath, repoFiles) : [];
3990
4751
  const testFiles = allTestFiles.slice(0, 8);
3991
4752
  const sourceFiles = repoFiles.filter(isSourceFile);
3992
4753
  const runnerFiles = repoFiles.filter(
@@ -4042,13 +4803,31 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
4042
4803
  const verificationScriptCount = verificationScriptComponents.filter((c) => c.count > 0).length;
4043
4804
  const usesLeanBuiltInTestRunner = packageJson != null && [...packageScripts.values()].some(
4044
4805
  (script) => LEAN_TEST_RUNNER_SCRIPT_PATTERN.test(script)
4045
- ) || 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));
4046
4809
  const hasHeavyTestDependency = packageJsonFiles.some(
4047
4810
  (manifest) => packageJsonHasHeavyTestDependency(repoPath, manifest)
4048
4811
  );
4049
4812
  const isLeanTestToolchain = usesLeanBuiltInTestRunner && !hasHeavyTestDependency;
4050
4813
  for (const file of testFiles) {
4051
- 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
+ }
4052
4831
  }
4053
4832
  for (const file of runnerFiles) {
4054
4833
  evidence.push(evidenceForRelative(repoPath, file, "test_run", "Configured test runner"));
@@ -4067,7 +4846,7 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
4067
4846
  );
4068
4847
  }
4069
4848
  const scheduledHealthWorkflow = ciWorkflows.find(
4070
- (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))
4071
4850
  );
4072
4851
  if (scheduledHealthWorkflow) {
4073
4852
  const isPublished = fileContains(
@@ -4168,8 +4947,30 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
4168
4947
  const nonHollowShare = withContentReadSignal(
4169
4948
  "A1",
4170
4949
  "non_hollow_test_share",
4171
- () => 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
+ )
4172
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
+ }
4173
4974
  return {
4174
4975
  criterionId: "A1",
4175
4976
  positiveEvidence: evidence,
@@ -4306,7 +5107,7 @@ function buildA1AuthenticatedAbsenceSignal(repoPath, proof, inventoryCount, inve
4306
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.`
4307
5108
  };
4308
5109
  }
4309
- 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) {
4310
5111
  const evidence = [];
4311
5112
  const findings = [];
4312
5113
  const gitignore = repoFiles.find((file) => basename(file) === ".gitignore");
@@ -4314,22 +5115,26 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
4314
5115
  const migrationFiles = repoFiles.filter(
4315
5116
  (file) => /(^|\/)(migrations?|drizzle|prisma)\//.test(file)
4316
5117
  );
4317
- const { committedSecret, ambiguousSecret, currentSecretFingerprintsByPath } = withContentReadSignal("A2", "secret_cleanliness", () => {
5118
+ const { committedSecret, ambiguousSecret, currentSecretFingerprintsByPath, placeholderSetAside } = withContentReadSignal("A2", "secret_cleanliness", () => {
4318
5119
  let match_;
4319
5120
  let ambiguous_;
5121
+ const setAsideFiles = [];
4320
5122
  for (const path of repoFiles) {
4321
5123
  const authoredProductionPath = useV24SecretContentContext ? isAuthoredProductionPath(path) : useV47Detectors ? isV47AuthoredProductionPath(path) : useV39Detectors ? isV39AuthoredProductionPath(path) : isAuthoredProductionPath(path);
4322
5124
  if ((useV24SecretContentContext ? isV24IgnoredScanFile(path) : isIgnoredScanFile(path, useV47Detectors)) || useV33Detectors && !authoredProductionPath) {
4323
5125
  continue;
4324
5126
  }
5127
+ const setAside = { count: 0 };
4325
5128
  if (useV24SecretContentContext) {
4326
5129
  const classified = findV24ClassifiedSecretInFile(
4327
5130
  repoPath,
4328
5131
  path,
4329
5132
  useV36Detectors,
4330
5133
  useV39Detectors,
4331
- useV47Detectors
5134
+ useV47Detectors,
5135
+ setAside
4332
5136
  );
5137
+ if (setAside.count > 0) setAsideFiles.push({ path, count: setAside.count });
4333
5138
  if (!classified) continue;
4334
5139
  if (classified.classification === "real") {
4335
5140
  match_ = { path, match: classified.match };
@@ -4344,8 +5149,10 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
4344
5149
  useV36Detectors,
4345
5150
  useV39Detectors,
4346
5151
  useV47Detectors,
4347
- useV23PemPrivateKeyGrammar
5152
+ useV23PemPrivateKeyGrammar,
5153
+ setAside
4348
5154
  );
5155
+ if (setAside.count > 0) setAsideFiles.push({ path, count: setAside.count });
4349
5156
  if (!found) continue;
4350
5157
  match_ = { path, match: found };
4351
5158
  break;
@@ -4376,7 +5183,10 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
4376
5183
  // abstention exists for repositories Cejel could not classify, not as a way for one
4377
5184
  // unclassifiable value to withhold a metric a confirmed leak already decided.
4378
5185
  ambiguousSecret: match_ ? void 0 : ambiguous_,
4379
- 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
4380
5190
  };
4381
5191
  });
4382
5192
  const historySecretScan = committedSecret && !useV27Detectors ? null : collectHistorySecretEvidence(
@@ -4410,10 +5220,13 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
4410
5220
  const hasDataLayer = migrationFiles.length > 0 || dbLayerImportFile !== void 0;
4411
5221
  const hasSecretsSurface = envExamples.length > 0 || committedEnvFilePath !== void 0 || gitignoreHasEnvRule || hasEnvInHistory;
4412
5222
  if (!hasDataLayer && !hasSecretsSurface && !hasConfirmedSecretFinding && !cryptoHygiene.hasSurface) {
4413
- return buildNotApplicableSignal(
4414
- "A2",
4415
- "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."
4416
- );
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
+ };
4417
5230
  }
4418
5231
  if (committedEnvFilePath) {
4419
5232
  evidence.push(
@@ -4480,7 +5293,12 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
4480
5293
  "A2",
4481
5294
  "env_handling_depth",
4482
5295
  () => evidence.push(
4483
- evidenceForRelative(repoPath, gitignoreEnvFile, "secret_scan", ".env files are gitignored")
5296
+ evidenceForRelative(
5297
+ repoPath,
5298
+ gitignoreEnvFile,
5299
+ "secret_scan",
5300
+ gitignoreEnvRuleLabel(repoFiles.filter(isCommittedEnvFormatPath))
5301
+ )
4484
5302
  )
4485
5303
  );
4486
5304
  }
@@ -4644,6 +5462,9 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
4644
5462
  findings.push(...cryptoHygiene.findings);
4645
5463
  if (evidence.length === 0 && findings.length === 0) return null;
4646
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
+ }
4647
5468
  const secretCleanliness = hasCriticalFinding ? 0 : 1;
4648
5469
  const abstainsSecretCleanliness = useV24SecretContentContext && ambiguousSecret != null && !hasCriticalFinding;
4649
5470
  const baseMetrics = [
@@ -4655,7 +5476,9 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
4655
5476
  1,
4656
5477
  isMultiTenant ? 0.25 : 0.7,
4657
5478
  "clean",
4658
- "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
4659
5482
  )
4660
5483
  ],
4661
5484
  metric(
@@ -4729,9 +5552,25 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
4729
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."
4730
5553
  ] : [],
4731
5554
  ...tenantIsolationUnsettled ? [WITAN_A2_TENANT_ISOLATION_UNSETTLED_DISCLOSURE] : []
4732
- ].join(" ")
5555
+ ].join(" "),
5556
+ ...placeholderSetAsideDisclosures2(placeholderSetAside)
4733
5557
  };
4734
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
+ }
4735
5574
  function collectCryptoHygieneEvidence(repoPath, implFiles, useV39Detectors = false) {
4736
5575
  const evidence = [];
4737
5576
  const findings = [];
@@ -4828,7 +5667,7 @@ function collectCryptoHygieneEvidence(repoPath, implFiles, useV39Detectors = fal
4828
5667
  }
4829
5668
  return { evidence, findings, hasSurface, clean: hasSurface && !insecureFound };
4830
5669
  }
4831
- function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, useV20ExplicitGaps = false, useV22PackageStartEntrypoint = false, useV23WithheldPathAbstention = false, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT, useV25InlineHealthRoute = false) {
5670
+ function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, useV20ExplicitGaps = false, useV22PackageStartEntrypoint = false, useV23WithheldPathAbstention = false, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT, useV25InlineHealthRoute = false, useV26BuildStepEvidence = false) {
4832
5671
  const v20DirectHttpEntrypoint = useV20ExplicitGaps ? findV20DirectHttpServerEntrypointFile(repoPath, repoFiles) : null;
4833
5672
  const v22PackageStartHttpEntrypoint = useV22PackageStartEntrypoint ? findV22PackageStartHttpServerEntrypointFile(repoPath, repoFiles) : null;
4834
5673
  const v20RuntimeContainer = useV20ExplicitGaps ? findRuntimeContainerEntrypointFile(repoPath, repoFiles, true) : null;
@@ -5045,13 +5884,22 @@ function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, us
5045
5884
  }
5046
5885
  }
5047
5886
  if (evidence.length === 0) return null;
5887
+ let buildStepSettlement;
5048
5888
  if (useV20ExplicitGaps) {
5049
5889
  const workspaceManifests = packageJson ? findDeclaredWorkspaceManifests(repoPath, repoFiles, packageJson) : NO_DECLARED_WORKSPACE_MANIFESTS;
5050
5890
  const hasBuildOrTypecheck = scripts.has("build") || scripts.has("typecheck") || workspaceManifests.declared > workspaceManifests.read.length || workspaceManifests.read.some((file) => {
5051
5891
  const workspaceScripts = readPackageScripts(join(repoPath, file));
5052
5892
  return workspaceScripts.has("build") || workspaceScripts.has("typecheck");
5053
5893
  });
5054
- if (packageJson && !hasBuildOrTypecheck) {
5894
+ buildStepSettlement = useV26BuildStepEvidence && packageJson ? settleV26BuildStepEvidence(
5895
+ repoPath,
5896
+ repoFiles,
5897
+ packageJson,
5898
+ workspaceManifests,
5899
+ workflows,
5900
+ (file) => !useV27Detectors || isAuthoredProductionPath(file)
5901
+ ) : void 0;
5902
+ if (packageJson && !hasBuildOrTypecheck && (buildStepSettlement === void 0 || buildStepSettlement === "fires")) {
5055
5903
  const workspaceRead = workspaceManifests.read.length;
5056
5904
  findings.push({
5057
5905
  severity: "info",
@@ -5192,10 +6040,11 @@ function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, us
5192
6040
  "Counts static rollback or migration-safety artifacts.",
5193
6041
  "saturating_count"
5194
6042
  )
5195
- ]
6043
+ ],
6044
+ ...buildStepSettlement === "unsettled" ? { notes: WITAN_A3_BUILD_STEP_UNSETTLED_DISCLOSURE } : {}
5196
6045
  };
5197
6046
  }
5198
- 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) {
5199
6048
  const evidence = [];
5200
6049
  const findings = [];
5201
6050
  const manifests = repoFiles.filter(isDependencyManifest);
@@ -5308,6 +6157,62 @@ function collectA4DependencyEvidence(repoPath, repoFiles, mono, useV27Detectors,
5308
6157
  )
5309
6158
  });
5310
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);
5311
6216
  const automationMetric = metric(
5312
6217
  "dependency_automation_ratio",
5313
6218
  "Dependency automation ratio",
@@ -5368,6 +6273,18 @@ function collectA4DependencyEvidence(repoPath, repoFiles, mono, useV27Detectors,
5368
6273
  "present",
5369
6274
  "Credited when present (reproducible dev/CI installs); a library without a committed lockfile is not penalized \u2014 consumers resolve their own trees."
5370
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
+ )
5371
6288
  ] : [],
5372
6289
  automationMetric,
5373
6290
  metric(
@@ -5385,8 +6302,373 @@ function collectA4DependencyEvidence(repoPath, repoFiles, mono, useV27Detectors,
5385
6302
  positiveEvidence: evidence,
5386
6303
  findings,
5387
6304
  metrics: isAppOrService ? appMetrics : libraryMetrics,
5388
- 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);
5389
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;
5390
6672
  }
5391
6673
  var LIBRARY_DEPENDENCY_COUNT_SANE_MAX = 120;
5392
6674
  function libraryDependencyCountSanity(specCount) {
@@ -5591,7 +6873,20 @@ function collectA5ClaimRealityEvidence(repoPath, repoFiles, useV27Detectors, use
5591
6873
  ...reconciliationUnsettled ? { notes: WITAN_A5_RECONCILIATION_UNSETTLED_DISCLOSURE } : {}
5592
6874
  };
5593
6875
  }
5594
- 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) {
5595
6890
  const workflows = repoFiles.filter(isCiWorkflow);
5596
6891
  const prTemplate = repoFiles.find(
5597
6892
  (file) => /(^|\/)(pull_request_template|PULL_REQUEST_TEMPLATE)(\.md$|\/[^/]+\.md$)/.test(file)
@@ -5600,15 +6895,21 @@ function collectB2PrTraceEvidence(repoPath, repoFiles) {
5600
6895
  (file) => /branch.*protection|review.*gate/i.test(file) || GITHUB_CODEOWNERS_PATH_PATTERN.test(file)
5601
6896
  );
5602
6897
  const citedWorkflows = workflows.slice(0, 3);
5603
- const workflowLabel = ciWorkflowCitationLabel(
5604
- "Pull-request CI workflow",
5605
- "counted",
5606
- workflows.length,
5607
- citedWorkflows.length,
5608
- 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
+ )
5609
6908
  );
5610
6909
  const evidence = [
5611
- ...citedWorkflows.map((file) => evidenceForRelative(repoPath, file, "ci_run", workflowLabel)),
6910
+ ...citedWorkflows.map(
6911
+ (file) => evidenceForRelative(repoPath, file, "ci_run", workflowLabel(file))
6912
+ ),
5612
6913
  ...prTemplate ? [evidenceForRelative(repoPath, prTemplate, "pull_request", "Pull request template")] : [],
5613
6914
  ...branchProtectionDoc ? [
5614
6915
  evidenceForRelative(
@@ -5620,6 +6921,9 @@ function collectB2PrTraceEvidence(repoPath, repoFiles) {
5620
6921
  ] : []
5621
6922
  ];
5622
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
+ }
5623
6927
  return {
5624
6928
  criterionId: "B2",
5625
6929
  positiveEvidence: evidence,
@@ -5663,18 +6967,46 @@ function collectB2PrTraceEvidence(repoPath, repoFiles) {
5663
6967
  ]
5664
6968
  };
5665
6969
  }
5666
- function collectB3CiDisciplineEvidence(repoPath, repoFiles, mono) {
6970
+ function collectB3CiDisciplineEvidence(repoPath, repoFiles, mono, useV27FalseAssuranceClosure = false) {
5667
6971
  const evidence = [];
5668
6972
  const packageJson = findRootPackageJson(repoFiles);
5669
6973
  const scripts = packageJson ? readPackageScripts(join(repoPath, packageJson)) : /* @__PURE__ */ new Map();
5670
6974
  const workflows = repoFiles.filter(isCiWorkflow);
5671
6975
  const workflow = workflows[0];
5672
6976
  const rootWorkflows = mono ? mono.sharedFiles.filter(isCiWorkflow) : [];
5673
- 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(
5674
6985
  "B3",
5675
6986
  "default_branch_ci_depth",
5676
- () => 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: [] }
5677
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
+ ) : [];
5678
7010
  if (packageJson && scripts.has("test")) {
5679
7011
  evidence.push(evidenceForRelative(repoPath, packageJson, "test_run", "Test script"));
5680
7012
  }
@@ -5756,10 +7088,10 @@ function collectB3CiDisciplineEvidence(repoPath, repoFiles, mono) {
5756
7088
  "ci_script_depth",
5757
7089
  () => ({
5758
7090
  repoCategoryHits: CI_COMMAND_CATEGORIES.map(
5759
- (pattern) => workflows.some((file) => fileContains(repoPath, file, pattern))
7091
+ (pattern) => workflows.filter(countsAsCi(repoPath)).some((file) => fileContains(repoPath, file, pattern))
5760
7092
  ),
5761
7093
  rootCategoryHits: mono ? CI_COMMAND_CATEGORIES.map(
5762
- (pattern) => rootWorkflows.some((file) => fileContains(mono.root, file, pattern))
7094
+ (pattern) => rootWorkflows.filter(countsAsCi(mono.root)).some((file) => fileContains(mono.root, file, pattern))
5763
7095
  ) : []
5764
7096
  })
5765
7097
  );
@@ -5805,10 +7137,655 @@ function collectB3CiDisciplineEvidence(repoPath, repoFiles, mono) {
5805
7137
  0.55,
5806
7138
  "workflows",
5807
7139
  "Counts CI workflows that target pull requests or the default branch, up to 4.",
5808
- "saturating_count"
7140
+ "saturating_count",
7141
+ b3UnshownCheckCount(unshownCheckWorkflows) > 0 ? { condition: b3UnshownCheckCondition(unshownCheckWorkflows) } : void 0
5809
7142
  )
5810
- ]
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;
5811
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;
5812
7789
  }
5813
7790
  function auditFreshnessCommitterYearFromGitResult(result, scanLimitations) {
5814
7791
  if (!result.ok) {
@@ -6167,7 +8144,6 @@ function hasCredibleTestScript(scripts) {
6167
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;
6168
8145
  var CI_LINT_COMMAND_PATTERN = /\b(eslint|pylint|flake8|black|ruff)\b/i;
6169
8146
  var CI_TYPECHECK_COMMAND_PATTERN = /\b(tsc|mypy|pyright)\b/i;
6170
- var SCHEDULE_TRIGGER_PATTERN = /(^|\n)\s*schedule:\s*\r?\n\s*-\s*cron:/;
6171
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;
6172
8148
  var EPHEMERAL_ARTIFACT_ONLY_PATTERN = /actions\/upload-artifact/i;
6173
8149
  var SCHEDULED_HEALTH_PATH_PATTERN = /(?:health|smoke|monitor|status|uptime|canary)/i;
@@ -6485,7 +8461,7 @@ function parseGitTrackedFiles(repoPath, output, includeHardExcluded, registerWit
6485
8461
  continue;
6486
8462
  }
6487
8463
  try {
6488
- const stat = lstatSync(fullPath);
8464
+ const stat = lstatSync2(fullPath);
6489
8465
  if (!stat.isFile()) {
6490
8466
  recordContentSkip(fullPath, "non_regular_file", true);
6491
8467
  withhold(file, "non_regular_file");
@@ -6560,19 +8536,19 @@ function resolveMonorepoContext(repoPath, scanLimitations) {
6560
8536
  }
6561
8537
  const top = topResult.stdout.trim();
6562
8538
  if (!top) return null;
6563
- const rootResolved = resolve(top);
8539
+ const rootResolved = resolve2(top);
6564
8540
  const repoResolved = (() => {
6565
8541
  try {
6566
- return realpathSync(repoPath);
8542
+ return realpathSync2(repoPath);
6567
8543
  } catch {
6568
- return resolve(repoPath);
8544
+ return resolve2(repoPath);
6569
8545
  }
6570
8546
  })();
6571
8547
  if (rootResolved === repoResolved) return null;
6572
- if (!repoResolved.startsWith(rootResolved + sep)) return null;
8548
+ if (!repoResolved.startsWith(rootResolved + sep2)) return null;
6573
8549
  const rootFiles = listGitTrackedFiles(rootResolved, scanLimitations);
6574
8550
  if (!rootFiles || rootFiles.length === 0) return null;
6575
- const subRel = repoResolved.slice(rootResolved.length + 1).split(sep).join("/");
8551
+ const subRel = repoResolved.slice(rootResolved.length + 1).split(sep2).join("/");
6576
8552
  const subPrefix = subRel ? `${subRel}/` : "";
6577
8553
  const sharedFiles = rootFiles.filter(
6578
8554
  (file) => (isLockfile(file) || isCiWorkflow(file) || isDependencyUpdateConfig(file)) && (subPrefix === "" || !file.startsWith(subPrefix))
@@ -6605,7 +8581,7 @@ function visitRepoDir(repoPath, dirPath, files, rootDevice) {
6605
8581
  }
6606
8582
  if (entry.isDirectory()) {
6607
8583
  try {
6608
- if (lstatSync(fullPath, { bigint: true }).dev !== BigInt(rootDevice)) {
8584
+ if (lstatSync2(fullPath, { bigint: true }).dev !== BigInt(rootDevice)) {
6609
8585
  recordContentSkip(fullPath, "denied_path");
6610
8586
  continue;
6611
8587
  }
@@ -6616,7 +8592,7 @@ function visitRepoDir(repoPath, dirPath, files, rootDevice) {
6616
8592
  visitRepoDir(repoPath, fullPath, files, rootDevice);
6617
8593
  continue;
6618
8594
  }
6619
- const repoRelativePath = relative(repoPath, fullPath);
8595
+ const repoRelativePath = relative2(repoPath, fullPath);
6620
8596
  if (!entry.isFile()) {
6621
8597
  recordContentSkip(fullPath, "non_regular_file");
6622
8598
  registerWithheldRepoPath(repoRelativePath, "non_regular_file");
@@ -6896,7 +8872,7 @@ function hasLiveFrameworkAssertion({ commentFree, code }) {
6896
8872
  function hasFrameworkSuiteLevelSkip({ code }) {
6897
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);
6898
8874
  }
6899
- function measureNonHollowTestShare(repoPath, files, useV17Detectors = false) {
8875
+ function measureNonHollowTestShare(repoPath, files, useV17Detectors = false, runWideSkip) {
6900
8876
  let nonHollowCount = 0;
6901
8877
  let ratedCount = 0;
6902
8878
  for (const file of files) {
@@ -6906,16 +8882,193 @@ function measureNonHollowTestShare(repoPath, files, useV17Detectors = false) {
6906
8882
  let maskedSource;
6907
8883
  const masked = () => maskedSource ??= maskCommentsAndStrings(contents);
6908
8884
  const frameworkSuiteSkipped = /\bIgnored?\b/.test(contents) && hasFrameworkSuiteLevelSkip(masked());
6909
- 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;
6910
8888
  if (nonHollow) {
6911
8889
  nonHollowCount += 1;
6912
8890
  ratedCount += 1;
6913
- } 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) {
6914
8892
  ratedCount += 1;
6915
8893
  }
6916
8894
  }
6917
8895
  return { nonHollowCount, ratedCount };
6918
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
+ }
6919
9072
  function countFilesContaining(repoPath, files, pattern) {
6920
9073
  return files.filter((file) => fileContains(repoPath, file, pattern)).length;
6921
9074
  }
@@ -7092,10 +9245,10 @@ function readPyprojectDependencySpecs(path) {
7092
9245
  const dep = rawDep.replace(/["'\s]/g, "");
7093
9246
  if (!dep) continue;
7094
9247
  const nameMatch = /^([A-Za-z0-9][\w.-]*)/.exec(dep);
7095
- const versionMatch = /([><=~!^]+[\d.]+)/.exec(dep);
9248
+ const versionConstraint = firstVersionConstraint(dep);
7096
9249
  const depName = nameMatch?.[1];
7097
9250
  if (depName) {
7098
- specs.push({ name: depName, version: versionMatch?.[1] ?? dep });
9251
+ specs.push({ name: depName, version: versionConstraint ?? dep });
7099
9252
  }
7100
9253
  }
7101
9254
  }
@@ -7869,7 +10022,7 @@ function isClaimArtifactDeclaringWorkflow(file) {
7869
10022
  function normaliseDeclaredClaimArtifactPath(candidate, baseDir) {
7870
10023
  if (!DECLARED_CLAIM_ARTIFACT_PATH_PATTERN.test(candidate)) return null;
7871
10024
  const joined = candidate.startsWith("/") ? posix.normalize(candidate.slice(1)) : posix.normalize(posix.join(baseDir, candidate));
7872
- const path = joined.replace(/\/+$/, "");
10025
+ const path = stripTrailingSlashes(joined);
7873
10026
  if (path === "" || path === "." || path === ".." || path.startsWith("../") || path.startsWith("/")) {
7874
10027
  return null;
7875
10028
  }
@@ -7879,7 +10032,7 @@ function markdownLinkTarget(target) {
7879
10032
  if (/^[a-z][a-z0-9+.-]*:/i.test(target) || target.startsWith("//") || target.startsWith("#")) {
7880
10033
  return null;
7881
10034
  }
7882
- const withoutSuffix = target.replace(/[?#].*$/, "");
10035
+ const withoutSuffix = stripFromQueryOrFragment(target);
7883
10036
  try {
7884
10037
  return decodeURIComponent(withoutSuffix);
7885
10038
  } catch {
@@ -7954,7 +10107,7 @@ function workflowClaimArtifactDeclarations(contents) {
7954
10107
  writeContinues = false;
7955
10108
  };
7956
10109
  contents.split(/\r?\n/).forEach((rawLine, index) => {
7957
- const line = rawLine.replace(/(^|\s)#.*$/, "");
10110
+ const line = stripHashCommentAtStartOrAfterBlank(rawLine);
7958
10111
  if (line.trim() === "") return;
7959
10112
  const indent = line.length - line.trimStart().length;
7960
10113
  if (stepsIndent !== null && (indent < stepsIndent || indent === stepsIndent && !line.trimStart().startsWith("- "))) {
@@ -8078,7 +10231,7 @@ function vitestCoveragePropertyCollects(contents) {
8078
10231
  return true;
8079
10232
  }
8080
10233
  function viteConfigCollectsCoverage(repoPath, file) {
8081
- const directory = dirname(file);
10234
+ const directory = dirname2(file);
8082
10235
  for (const extension of ["ts", "mts", "cts", "js", "mjs", "cjs"]) {
8083
10236
  const sibling = join(repoPath, directory, `vitest.config.${extension}`);
8084
10237
  if (!isRegularFile(sibling)) continue;
@@ -8778,7 +10931,7 @@ function containsPotentialCoverageFlag(command) {
8778
10931
  function reachableShellWrapper(words, repoFiles) {
8779
10932
  const executable = basename(words[0] ?? "").toLowerCase();
8780
10933
  const candidate = /^(?:ba|da|k|z)?sh$/u.test(executable) ? words[1] : words[0];
8781
- if (!candidate || !/(?:^|\/)\.?[^\s]*\.sh$/i.test(candidate)) return void 0;
10934
+ if (!candidate || !isShellScriptPath(candidate)) return void 0;
8782
10935
  const normalized = candidate.replace(/^\.\//u, "");
8783
10936
  if (normalized.startsWith("/") || normalized.split("/").includes("..")) return void 0;
8784
10937
  return repoFiles.has(normalized) ? normalized : void 0;
@@ -9346,7 +11499,7 @@ function packageJsonDescribesPackagedApplication(repoPath, manifest, repoFiles)
9346
11499
  return hasDesktopRuntime && hasPackagingCommand && hasApplicationMetadata;
9347
11500
  }
9348
11501
  function findTauriApplicationMetadata(repoPath, manifest, repoFiles) {
9349
- const manifestDirectory = dirname(manifest);
11502
+ const manifestDirectory = dirname2(manifest);
9350
11503
  const prefix = manifestDirectory === "." ? "" : `${manifestDirectory}/`;
9351
11504
  const candidates = [`${prefix}src-tauri/tauri.conf.json`, `${prefix}tauri.conf.json`];
9352
11505
  return candidates.some((file) => {
@@ -9396,31 +11549,38 @@ function findRootPackageJson(repoFiles) {
9396
11549
  var WORKSPACE_PATTERN_LIMIT = 64;
9397
11550
  var WORKSPACE_PACKAGE_MANIFEST_LIMIT = 200;
9398
11551
  var NO_DECLARED_WORKSPACE_MANIFESTS = { read: [], declared: 0 };
11552
+ var UNREADABLE_WORKSPACE_DECLARATION = {
11553
+ read: [],
11554
+ declared: 0,
11555
+ unreadable: true
11556
+ };
9399
11557
  function findDeclaredWorkspaceManifests(repoPath, repoFiles, rootPackageJson) {
9400
11558
  const patterns = [];
9401
11559
  if (rootPackageJson) {
9402
11560
  const declared = readPackageJsonWorkspacePatterns(join(repoPath, rootPackageJson));
9403
- if (declared === null) return NO_DECLARED_WORKSPACE_MANIFESTS;
11561
+ if (declared === null) return UNREADABLE_WORKSPACE_DECLARATION;
9404
11562
  patterns.push(...declared);
9405
11563
  }
9406
11564
  if (repoFiles.includes("pnpm-workspace.yaml")) {
9407
11565
  const declared = readPnpmWorkspacePatterns(join(repoPath, "pnpm-workspace.yaml"));
9408
- if (declared === null) return NO_DECLARED_WORKSPACE_MANIFESTS;
11566
+ if (declared === null) return UNREADABLE_WORKSPACE_DECLARATION;
9409
11567
  patterns.push(...declared);
9410
11568
  }
9411
- if (patterns.length === 0 || patterns.length > WORKSPACE_PATTERN_LIMIT) {
9412
- return NO_DECLARED_WORKSPACE_MANIFESTS;
9413
- }
11569
+ if (patterns.length === 0) return NO_DECLARED_WORKSPACE_MANIFESTS;
11570
+ if (patterns.length > WORKSPACE_PATTERN_LIMIT) return UNREADABLE_WORKSPACE_DECLARATION;
9414
11571
  const include = [];
9415
11572
  const exclude = [];
11573
+ let unparsedPattern = false;
9416
11574
  for (const pattern of patterns) {
9417
11575
  const negated = pattern.startsWith("!");
9418
11576
  const matcher = workspaceGlobMatcher(negated ? pattern.slice(1) : pattern);
9419
11577
  if (negated) {
9420
- if (matcher === null) return NO_DECLARED_WORKSPACE_MANIFESTS;
11578
+ if (matcher === null) return UNREADABLE_WORKSPACE_DECLARATION;
9421
11579
  exclude.push(matcher);
9422
11580
  } else if (matcher !== null) {
9423
11581
  include.push(matcher);
11582
+ } else {
11583
+ unparsedPattern = true;
9424
11584
  }
9425
11585
  }
9426
11586
  const matched = repoFiles.filter((file) => {
@@ -9429,7 +11589,11 @@ function findDeclaredWorkspaceManifests(repoPath, repoFiles, rootPackageJson) {
9429
11589
  const dir = file.slice(0, -"/package.json".length);
9430
11590
  return include.some((matches) => matches(dir)) && !exclude.some((matches) => matches(dir));
9431
11591
  }).sort();
9432
- return { read: matched.slice(0, WORKSPACE_PACKAGE_MANIFEST_LIMIT), declared: matched.length };
11592
+ return {
11593
+ read: matched.slice(0, WORKSPACE_PACKAGE_MANIFEST_LIMIT),
11594
+ declared: matched.length,
11595
+ ...unparsedPattern ? { unreadable: true } : {}
11596
+ };
9433
11597
  }
9434
11598
  function readPackageJsonWorkspacePatterns(packageJsonPath) {
9435
11599
  if (!isRegularFile(packageJsonPath)) return [];
@@ -9478,7 +11642,7 @@ function readPnpmWorkspacePatterns(yamlPath) {
9478
11642
  return patterns;
9479
11643
  }
9480
11644
  function stripYamlLineComment(text) {
9481
- return /^\s*#/.test(text) ? "" : text.replace(/\s+#.*$/, "");
11645
+ return /^\s*#/.test(text) ? "" : stripTrailingHashComment(text);
9482
11646
  }
9483
11647
  function yamlScalar(raw) {
9484
11648
  const single = /^'((?:[^']|'')*)'\s*(?:#.*)?$/.exec(raw);
@@ -9492,7 +11656,7 @@ var WORKSPACE_GLOB_MAX_LENGTH = 256;
9492
11656
  var WORKSPACE_GLOB_MAX_SEGMENTS = 32;
9493
11657
  function workspaceGlobMatcher(pattern) {
9494
11658
  if (pattern.length > WORKSPACE_GLOB_MAX_LENGTH) return null;
9495
- const normalised = pattern.trim().replace(/^\.\//, "").replace(/\/+$/, "");
11659
+ const normalised = stripTrailingSlashes(pattern.trim().replace(/^\.\//, ""));
9496
11660
  if (normalised === "" || normalised.startsWith("/") || /(^|\/)\.\.?(\/|$)/.test(normalised) || /[[\]{}()!+@\\]/.test(normalised)) {
9497
11661
  return null;
9498
11662
  }
@@ -9601,7 +11765,7 @@ function countDelegatedCiSteps(root, workflows) {
9601
11765
  continue;
9602
11766
  }
9603
11767
  if (!uses.startsWith("./") || /^\.\/\.github\/workflows\//.test(uses)) continue;
9604
- const directory = uses.slice(2).replace(/\/+$/u, "");
11768
+ const directory = stripTrailingSlashes(uses.slice(2));
9605
11769
  if (directory.length === 0 || directory.split("/").includes("..")) continue;
9606
11770
  if (isRegularFile(join(root, directory, "action.yml")) || isRegularFile(join(root, directory, "action.yaml"))) {
9607
11771
  compositeActions += 1;
@@ -9621,7 +11785,7 @@ function extractWorkflowUsesValues(contents) {
9621
11785
  }
9622
11786
  const match = /^\s*(?:-\s*)?uses\s*:\s*(.*)$/u.exec(line);
9623
11787
  if (match) {
9624
- const value = (match[1] ?? "").replace(/\s+#.*$/u, "").trim();
11788
+ const value = stripTrailingHashComment(match[1] ?? "").trim();
9625
11789
  const quoted = /^(?:"(.*)"|'(.*)')$/u.exec(value);
9626
11790
  values.push(quoted ? quoted[1] ?? quoted[2] ?? "" : value);
9627
11791
  continue;
@@ -9639,6 +11803,263 @@ function unreadCiWorkflowReasons(includes) {
9639
11803
  }
9640
11804
  return unread;
9641
11805
  }
11806
+ var BUILD_STEP_EXECUTABLES = /* @__PURE__ */ new Set([
11807
+ "babel",
11808
+ "esbuild",
11809
+ "ncc",
11810
+ "rollup",
11811
+ "svelte-check",
11812
+ "swc",
11813
+ "tsc",
11814
+ "tsup",
11815
+ "vue-tsc",
11816
+ "webpack",
11817
+ "webpack-cli"
11818
+ ]);
11819
+ var BUILD_STEP_SUBCOMMANDS = /* @__PURE__ */ new Map([
11820
+ ["bun", "build"],
11821
+ ["nest", "build"],
11822
+ ["next", "build"],
11823
+ ["ng", "build"],
11824
+ ["parcel", "build"],
11825
+ ["vite", "build"]
11826
+ ]);
11827
+ var BUILD_STEP_TASK_RUNNERS = /* @__PURE__ */ new Set([
11828
+ "bun",
11829
+ "gmake",
11830
+ "just",
11831
+ "lerna",
11832
+ "make",
11833
+ "npm",
11834
+ "nx",
11835
+ "pnpm",
11836
+ "turbo",
11837
+ "yarn"
11838
+ ]);
11839
+ var BUILD_STEP_TARGET_NAME_PATTERN = /^(?:build|typecheck)$/u;
11840
+ var BUILD_STEP_TARGET_ARGUMENT_PATTERN = /^(?:[^\s:]+:)?(?:build|typecheck)$/u;
11841
+ var TYPESCRIPT_SOURCE_PATTERN = /\.(?:ts|tsx|mts|cts)$/u;
11842
+ var TYPESCRIPT_DECLARATION_PATTERN = /\.d\.(?:ts|mts|cts)$/u;
11843
+ var TSCONFIG_PATTERN = /(^|\/)tsconfig[^/]*\.json$/u;
11844
+ var BUILD_STEP_DOCKERFILE_PATTERN = /(^|\/)(?:dockerfile|containerfile)(?:\.[^/]*)?$|[^/]\.dockerfile$/iu;
11845
+ function settleV26BuildStepEvidence(repoPath, repoFiles, packageJson, workspaceManifests, workflows, includesWorkflow) {
11846
+ const nestedPackageDirs = repoFiles.filter((file) => file !== packageJson && /(^|\/)package\.json$/u.test(file)).map((file) => file.slice(0, -"package.json".length));
11847
+ const hasSomethingToBuild = repoFiles.some(
11848
+ (file) => isAuthoredProductionPath(file) && (TYPESCRIPT_SOURCE_PATTERN.test(file) && !TYPESCRIPT_DECLARATION_PATTERN.test(file) || TSCONFIG_PATTERN.test(file)) && !nestedPackageDirs.some((dir) => file.startsWith(dir))
11849
+ );
11850
+ if (!hasSomethingToBuild) return "nothing-to-build";
11851
+ let unsettled = false;
11852
+ const commandRunsStep = (command) => {
11853
+ const state = classifyBuildStepCommand(command);
11854
+ if (state === "unparseable") unsettled = true;
11855
+ return state === "step";
11856
+ };
11857
+ for (const manifest of [packageJson, ...workspaceManifests.read]) {
11858
+ const scripts = readBuildStepPackageScripts(join(repoPath, manifest));
11859
+ if (scripts === null) {
11860
+ unsettled = true;
11861
+ continue;
11862
+ }
11863
+ for (const [name, command] of scripts) {
11864
+ if (BUILD_STEP_TARGET_NAME_PATTERN.test(name) || commandRunsStep(command)) return "step-found";
11865
+ }
11866
+ }
11867
+ for (const workflow of workflows) {
11868
+ const fullPath = join(repoPath, workflow);
11869
+ if (!/^\.github\/workflows\//u.test(workflow) || !isRegularFile(fullPath)) {
11870
+ unsettled = true;
11871
+ continue;
11872
+ }
11873
+ if (extractCiRunCommands(readRepoText(fullPath, "utf8")).some(commandRunsStep)) {
11874
+ return "step-found";
11875
+ }
11876
+ }
11877
+ for (const file of repoFiles.filter((path) => BUILD_STEP_DOCKERFILE_PATTERN.test(path))) {
11878
+ const fullPath = join(repoPath, file);
11879
+ if (!isRegularFile(fullPath)) {
11880
+ unsettled = true;
11881
+ continue;
11882
+ }
11883
+ const run = dockerfileRunCommands(readRepoText(fullPath, "utf8"));
11884
+ if (run.unparseable) unsettled = true;
11885
+ if (run.commands.some(commandRunsStep)) return "step-found";
11886
+ }
11887
+ for (const file of repoFiles.filter(isRecipeFile)) {
11888
+ const fullPath = join(repoPath, file);
11889
+ if (!isRegularFile(fullPath)) {
11890
+ unsettled = true;
11891
+ continue;
11892
+ }
11893
+ for (const [name, target] of readRecipeTargets(readRepoText(fullPath, "utf8"))) {
11894
+ if (BUILD_STEP_TARGET_NAME_PATTERN.test(name)) return "step-found";
11895
+ if (target.commands.some((command) => commandRunsStep(command.replace(/\\$/u, "")))) {
11896
+ return "step-found";
11897
+ }
11898
+ }
11899
+ }
11900
+ if (workspaceManifests.declared > workspaceManifests.read.length || workspaceManifests.unreadable) {
11901
+ unsettled = true;
11902
+ }
11903
+ if (unreadCiWorkflowReasons(includesWorkflow).size > 0) unsettled = true;
11904
+ const delegated = countDelegatedCiSteps(repoPath, workflows);
11905
+ if (delegated.crossRepositoryReusableWorkflows + delegated.compositeActions > 0) unsettled = true;
11906
+ if (withheldRepoPaths().some(
11907
+ ({ path }) => BUILD_STEP_DOCKERFILE_PATTERN.test(path) || isRecipeFile(path) || /(^|\/)package\.json$/u.test(path) && isAuthoredProductionPath(path)
11908
+ )) {
11909
+ unsettled = true;
11910
+ }
11911
+ return unsettled ? "unsettled" : "fires";
11912
+ }
11913
+ function readBuildStepPackageScripts(packageJsonPath) {
11914
+ if (!isRegularFile(packageJsonPath)) return null;
11915
+ const parsed = parseJsonObject(readRepoText(packageJsonPath, "utf8"));
11916
+ if (parsed === null) return null;
11917
+ if (parsed.scripts === void 0) return /* @__PURE__ */ new Map();
11918
+ if (!isRecord(parsed.scripts)) return null;
11919
+ const entries = Object.entries(parsed.scripts);
11920
+ if (!entries.every((entry) => typeof entry[1] === "string")) return null;
11921
+ return new Map(entries);
11922
+ }
11923
+ function dockerfileRunCommands(contents) {
11924
+ if (/^#\s*escape\s*=/imu.test(contents)) return { commands: [], unparseable: true };
11925
+ const lines = contents.split(/\r?\n/u);
11926
+ const commands = [];
11927
+ let unparseable = false;
11928
+ for (let index = 0; index < lines.length; index += 1) {
11929
+ const match = /^\s*RUN(?:\s+(.*))?$/iu.exec(lines[index] ?? "");
11930
+ if (!match) continue;
11931
+ const chunks = [match[1] ?? ""];
11932
+ while (endsWithLineContinuation(chunks) && index + 1 < lines.length) {
11933
+ index += 1;
11934
+ const next = lines[index] ?? "";
11935
+ if (/^\s*#/u.test(next)) continue;
11936
+ dropLineContinuation(chunks);
11937
+ chunks.push(" ", next.trim());
11938
+ }
11939
+ let instruction = chunks.join("");
11940
+ instruction = instruction.replace(/^(?:--[A-Za-z][\w-]*(?:=\S*)?\s+)*/u, "").trim();
11941
+ if (instruction === "" || /<<-?\s*["']?[A-Za-z_]/u.test(instruction)) {
11942
+ unparseable = true;
11943
+ continue;
11944
+ }
11945
+ if (!instruction.startsWith("[")) {
11946
+ commands.push(instruction);
11947
+ continue;
11948
+ }
11949
+ const execForm = dockerfileExecFormCommand(instruction);
11950
+ if (execForm === null) unparseable = true;
11951
+ else commands.push(execForm);
11952
+ }
11953
+ return { commands, unparseable };
11954
+ }
11955
+ function endsWithLineContinuation(chunks) {
11956
+ for (let c = chunks.length - 1; c >= 0; c -= 1) {
11957
+ const chunk = chunks[c] ?? "";
11958
+ for (let i = chunk.length - 1; i >= 0; i -= 1) {
11959
+ const character = chunk.charAt(i);
11960
+ if (/\s/u.test(character)) continue;
11961
+ return character === "\\";
11962
+ }
11963
+ }
11964
+ return false;
11965
+ }
11966
+ function dropLineContinuation(chunks) {
11967
+ while (chunks.length > 0) {
11968
+ const chunk = chunks[chunks.length - 1] ?? "";
11969
+ let i = chunk.length - 1;
11970
+ while (i >= 0 && /\s/u.test(chunk.charAt(i))) i -= 1;
11971
+ if (i < 0) {
11972
+ chunks.pop();
11973
+ continue;
11974
+ }
11975
+ chunks[chunks.length - 1] = chunk.slice(0, i);
11976
+ return;
11977
+ }
11978
+ }
11979
+ function dockerfileExecFormCommand(instruction) {
11980
+ let parsed;
11981
+ try {
11982
+ parsed = JSON.parse(instruction);
11983
+ } catch {
11984
+ return null;
11985
+ }
11986
+ if (!Array.isArray(parsed) || parsed.length === 0 || !parsed.every((part) => typeof part === "string")) {
11987
+ return null;
11988
+ }
11989
+ return parsed.map(
11990
+ (part) => /^[A-Za-z0-9_@%+=:,./-]+$/u.test(part) ? part : `'${part.replaceAll("'", "'\\''")}'`
11991
+ ).join(" ");
11992
+ }
11993
+ function classifyBuildStepCommand(command) {
11994
+ return buildStepCommandState(command, 0);
11995
+ }
11996
+ function buildStepCommandState(command, depth) {
11997
+ if (depth > 4) return "unparseable";
11998
+ const states = splitShellCommandSegments(command).map(
11999
+ (segment) => buildStepSegmentState(segment, depth)
12000
+ );
12001
+ if (states.includes("step")) return "step";
12002
+ return states.includes("unparseable") ? "unparseable" : "none";
12003
+ }
12004
+ function buildStepSegmentState(raw, depth) {
12005
+ let segment = stripUnbalancedShellParentheses(raw.trim());
12006
+ for (; ; ) {
12007
+ const keyword = /^(?:if|then|else|elif|do|while|until|exec|!|\{)(?:\s+|$)/u.exec(segment);
12008
+ if (!keyword) break;
12009
+ segment = segment.slice(keyword[0].length);
12010
+ }
12011
+ if (segment === "" || /^(?:fi|done|esac|\})$/u.test(segment)) return "none";
12012
+ const normalized = normalizeCoverageCommandPrefixes(segment);
12013
+ if (normalized.splitShellOperators && splitShellCommandSegments(normalized.segment).length > 1) {
12014
+ return buildStepCommandState(normalized.segment, depth + 1);
12015
+ }
12016
+ if (/\b(?:npm|pnpm|yarn|bun)\s+run\s+(?:\$|["']\$|`)/u.test(normalized.segment)) {
12017
+ return "unparseable";
12018
+ }
12019
+ const words = strictShellWords(normalized.segment);
12020
+ if (words === null) return "unparseable";
12021
+ return words.length === 0 ? "none" : buildStepWordsState(words);
12022
+ }
12023
+ function buildStepWordsState(words) {
12024
+ const first = words[0] === "$(MAKE)" || words[0] === "${MAKE}" ? "make" : words[0] ?? "";
12025
+ if (isUnresolvedCommandWord(first)) return "unparseable";
12026
+ const runner = buildStepCommandName(first);
12027
+ if (BUILD_STEP_TASK_RUNNERS.has(runner) && words.slice(1).some((word) => BUILD_STEP_TARGET_ARGUMENT_PATTERN.test(word))) {
12028
+ return "step";
12029
+ }
12030
+ const command = runner === "bunx" ? words.slice(1) : runner === "bun" && words[1] === "x" ? words.slice(2) : unwrapPackageManagerExecutable(words);
12031
+ const executable = command[0] ?? "";
12032
+ if (isUnresolvedCommandWord(executable)) return "unparseable";
12033
+ const name = buildStepCommandName(executable);
12034
+ if (BUILD_STEP_EXECUTABLES.has(name)) return "step";
12035
+ const subcommand = BUILD_STEP_SUBCOMMANDS.get(name);
12036
+ return subcommand !== void 0 && command.slice(1).includes(subcommand) ? "step" : "none";
12037
+ }
12038
+ function isUnresolvedCommandWord(word) {
12039
+ return word.startsWith("$") || word.includes("`");
12040
+ }
12041
+ function buildStepCommandName(word) {
12042
+ return basename(word).replace(/\.(?:cmd|exe)$/iu, "").toLowerCase();
12043
+ }
12044
+ function stripUnbalancedShellParentheses(segment) {
12045
+ const opens = segment.split("(").length - 1;
12046
+ const closes = segment.split(")").length - 1;
12047
+ if (opens > closes) return segment.replace(/^\(+\s*/u, "");
12048
+ if (closes > opens) return stripTrailingCloseParentheses(segment);
12049
+ return segment;
12050
+ }
12051
+ function strictShellWords(segment) {
12052
+ const words = [];
12053
+ let offset = 0;
12054
+ for (; ; ) {
12055
+ while (offset < segment.length && /\s/u.test(segment[offset] ?? "")) offset += 1;
12056
+ if (offset >= segment.length) return words;
12057
+ const scanned = scanShellWord(segment, offset);
12058
+ if (!scanned) return null;
12059
+ if (scanned.word !== "") words.push(scanned.word);
12060
+ offset = scanned.end;
12061
+ }
12062
+ }
9642
12063
  function rootOnlyPackageScriptsDisclosure(repoFiles, rootPackageJson) {
9643
12064
  const others = repoFiles.filter(
9644
12065
  (file) => file !== rootPackageJson && /(^|\/)package\.json$/.test(file) && isAuthoredProductionPath(file)
@@ -9662,7 +12083,7 @@ function workspacePackageScriptsDisclosure(repoFiles, rootPackageJson, workspace
9662
12083
  }
9663
12084
  function isRegularFile(path) {
9664
12085
  try {
9665
- return lstatSync(path).isFile();
12086
+ return lstatSync2(path).isFile();
9666
12087
  } catch {
9667
12088
  return false;
9668
12089
  }
@@ -9672,6 +12093,11 @@ function fileContains(repoPath, file, pattern) {
9672
12093
  if (!isRegularFile(fullPath)) return false;
9673
12094
  return pattern.test(readRepoText(fullPath, "utf8"));
9674
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
+ }
9675
12101
  var HASH_COMMENT_SOURCE_PATTERN = /\.(?:py|rb)$/i;
9676
12102
  function stripCodeComments(file, contents) {
9677
12103
  const withoutComments = contents.replace(/\/\*[\s\S]*?\*\//g, (block) => block.replace(/[^\n]/g, "")).replace(/(?<!:)\/\/.*$/gm, "");
@@ -9730,7 +12156,7 @@ ${contextLines}`
9730
12156
  line,
9731
12157
  valueLength: value.length,
9732
12158
  characterClasses: characterClasses || "other",
9733
- valueFingerprint: createHash3("sha256").update(value).digest("hex"),
12159
+ valueFingerprint: createHash4("sha256").update(value).digest("hex"),
9734
12160
  kind: "pem_private_key"
9735
12161
  };
9736
12162
  }
@@ -9751,7 +12177,7 @@ function findPemPrivateKeyAssignment(contents, file) {
9751
12177
  if (!isBarePemKeyFile(file)) return null;
9752
12178
  return scanForPemPrivateKeyMatch(BARE_PEM_PRIVATE_KEY_PATTERN, contents, lines, false);
9753
12179
  }
9754
- 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) {
9755
12181
  const fullPath = join(repoPath, file);
9756
12182
  if (!isRegularFile(fullPath)) return null;
9757
12183
  const contents = readRepoText(fullPath, "utf8");
@@ -9764,7 +12190,12 @@ function findCommittedSecretInFile(repoPath, file, useV36Detectors = false, useV
9764
12190
  useV47Detectors
9765
12191
  );
9766
12192
  const secret = findRealSecretAssignment(secretScanContents, /* @__PURE__ */ new Set(), {
9767
- allowCredentialNamedDigest: useV36Detectors
12193
+ allowCredentialNamedDigest: useV36Detectors,
12194
+ ...setAside ? {
12195
+ observeValue: (value, outcome) => {
12196
+ if (isPlaceholderSetAsideSecret(value, outcome)) setAside.count += 1;
12197
+ }
12198
+ } : {}
9768
12199
  });
9769
12200
  if (secret) return secret;
9770
12201
  if (useV23PemPrivateKeyGrammar) {
@@ -9779,7 +12210,7 @@ function findCommittedSecretInFile(repoPath, file, useV36Detectors = false, useV
9779
12210
  }
9780
12211
  return useV36Detectors && !isTestCredentialConfiguration ? findDefaultAdministrativeCredential(secretScanContents) : null;
9781
12212
  }
9782
- function findV24ClassifiedSecretInFile(repoPath, file, useV36Detectors, useV39Detectors, useV47Detectors) {
12213
+ function findV24ClassifiedSecretInFile(repoPath, file, useV36Detectors, useV39Detectors, useV47Detectors, setAside) {
9783
12214
  const fullPath = join(repoPath, file);
9784
12215
  if (!isRegularFile(fullPath)) return null;
9785
12216
  const contents = readRepoText(fullPath, "utf8");
@@ -9793,7 +12224,7 @@ function findV24ClassifiedSecretInFile(repoPath, file, useV36Detectors, useV39De
9793
12224
  true
9794
12225
  );
9795
12226
  const rawLines = contents.split(/\r?\n/);
9796
- const classifyFor = (wanted) => (value, identifier, line) => {
12227
+ const classifyFor = (wanted, countSetAside = false) => (value, identifier, line) => {
9797
12228
  const outcome = classifyV24SecretShapedValue(
9798
12229
  value,
9799
12230
  identifier,
@@ -9801,11 +12232,14 @@ function findV24ClassifiedSecretInFile(repoPath, file, useV36Detectors, useV39De
9801
12232
  rawLines,
9802
12233
  useV36Detectors
9803
12234
  );
12235
+ if (countSetAside && setAside && isPlaceholderSetAsideSecret(value, outcome)) {
12236
+ setAside.count += 1;
12237
+ }
9804
12238
  return outcome.classification === wanted ? outcome : V24_NO_FINDING;
9805
12239
  };
9806
12240
  const real = findRealSecretAssignment(secretScanContents, /* @__PURE__ */ new Set(), {
9807
12241
  allowCredentialNamedDigest: useV36Detectors,
9808
- classifyValue: classifyFor("real")
12242
+ classifyValue: classifyFor("real", true)
9809
12243
  });
9810
12244
  if (real) return { match: real, classification: "real" };
9811
12245
  if (useV39Detectors && isEnvHistoryPath(file, useV36Detectors)) {
@@ -9997,8 +12431,9 @@ function findRealSecretAssignments(contents, excludedValueFingerprints = /* @__P
9997
12431
  const value = contents.slice(valueStart, cursor).replace(/[,;)]$/, "");
9998
12432
  nextSearchIndex = cursor;
9999
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);
10000
12435
  if (outcome.classification !== "no_finding") {
10001
- const valueFingerprint = createHash3("sha256").update(value).digest("hex");
12436
+ const valueFingerprint = createHash4("sha256").update(value).digest("hex");
10002
12437
  const characterClasses = [
10003
12438
  /[a-z]/.test(value) ? "lower" : "",
10004
12439
  /[A-Z]/.test(value) ? "upper" : "",
@@ -10055,7 +12490,7 @@ function findWeakExplicitEnvCredential(contents) {
10055
12490
  line: index + 1,
10056
12491
  valueLength: value.length,
10057
12492
  characterClasses: characterClasses || "other",
10058
- valueFingerprint: createHash3("sha256").update(value).digest("hex")
12493
+ valueFingerprint: createHash4("sha256").update(value).digest("hex")
10059
12494
  };
10060
12495
  }
10061
12496
  return null;
@@ -10095,7 +12530,7 @@ function findDatabaseUrlUserinfoCredential(contents) {
10095
12530
  line: index + 1,
10096
12531
  valueLength: password.length,
10097
12532
  characterClasses: characterClasses || "other",
10098
- valueFingerprint: createHash3("sha256").update(password).digest("hex")
12533
+ valueFingerprint: createHash4("sha256").update(password).digest("hex")
10099
12534
  };
10100
12535
  }
10101
12536
  return null;
@@ -10114,7 +12549,7 @@ function findDefaultAdministrativeCredential(contents) {
10114
12549
  line: index + 1,
10115
12550
  valueLength: value.length,
10116
12551
  characterClasses: "lower",
10117
- valueFingerprint: createHash3("sha256").update(value).digest("hex"),
12552
+ valueFingerprint: createHash4("sha256").update(value).digest("hex"),
10118
12553
  kind: "default_admin"
10119
12554
  };
10120
12555
  }
@@ -10390,6 +12825,45 @@ function isPlaceholderSecretValue(value) {
10390
12825
  const normalized = value.trim().toLowerCase();
10391
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);
10392
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
+ }
10393
12867
  function isFillerRunSecretValue(value) {
10394
12868
  const candidates = [value];
10395
12869
  const sepIdx = Math.max(value.lastIndexOf("_"), value.lastIndexOf("-"));
@@ -10566,7 +13040,7 @@ function hasSuspiciousDependencies(repoPath, manifest, useV47Detectors = false)
10566
13040
  }
10567
13041
  function readRecentCommits(repoPath) {
10568
13042
  const historyResult = execGit(
10569
- ["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"],
10570
13044
  {
10571
13045
  cwd: repoPath
10572
13046
  }
@@ -10602,7 +13076,7 @@ function evidenceForRelative(repoPath, path, kind, label) {
10602
13076
  path,
10603
13077
  ...contents ? {
10604
13078
  line: firstMeaningfulLine(contents),
10605
- contentHash: createHash3("sha256").update(contents).digest("hex")
13079
+ contentHash: createHash4("sha256").update(contents).digest("hex")
10606
13080
  } : {}
10607
13081
  };
10608
13082
  }
@@ -10614,7 +13088,7 @@ function evidenceForRelativeAtLine(repoPath, path, kind, label, line) {
10614
13088
  label,
10615
13089
  path,
10616
13090
  line,
10617
- ...contents ? { contentHash: createHash3("sha256").update(contents).digest("hex") } : {}
13091
+ ...contents ? { contentHash: createHash4("sha256").update(contents).digest("hex") } : {}
10618
13092
  };
10619
13093
  }
10620
13094
  function metric(name, label, value, max, weight, unit, description, kind, presentation) {
@@ -10680,15 +13154,6 @@ function hasEnvPathInGitHistory(repoPath, scanLimitations) {
10680
13154
  // src/witan/public-scan.ts
10681
13155
  init_esm_shims();
10682
13156
 
10683
- // src/witan/history-depth.ts
10684
- init_esm_shims();
10685
- 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).";
10686
- function detectHistoryLimitations(repoPath) {
10687
- const result = execGit(["rev-parse", "--is-shallow-repository"], { cwd: repoPath });
10688
- if (!result.ok || result.stdout.trim() !== "true") return void 0;
10689
- return [SHALLOW_HISTORY_LIMITATION];
10690
- }
10691
-
10692
13157
  // src/witan/ingest.ts
10693
13158
  init_esm_shims();
10694
13159
  import { lstatSync as lstatSync3, opendirSync, readFileSync as readFileSync4, realpathSync as realpathSync3 } from "fs";
@@ -10716,54 +13181,7 @@ function clampFinding(finding) {
10716
13181
  // src/witan/generic-adapter.ts
10717
13182
  init_esm_shims();
10718
13183
  import { readFileSync } from "fs";
10719
-
10720
- // src/witan/ingest-files.ts
10721
- init_esm_shims();
10722
- import { lstatSync as lstatSync2, realpathSync as realpathSync2 } from "fs";
10723
- import { dirname as dirname2, isAbsolute, relative as relative2, resolve as resolve2, sep as sep2 } from "path";
10724
- var MAX_INGEST_DOCUMENT_BYTES = 8 * 1024 * 1024;
10725
- function isContainedPath(candidate, directory) {
10726
- const relativePath = relative2(directory, candidate);
10727
- return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${sep2}`) && !isAbsolute(relativePath);
10728
- }
10729
- function isMissingIngestPath(error) {
10730
- return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
10731
- }
10732
- function resolveIngestFilePath(filePath, inputDirectory = dirname2(filePath)) {
10733
- const lexicalDirectory = resolve2(inputDirectory);
10734
- const lexicalCandidate = resolve2(filePath);
10735
- const directoryStat = lstatSync2(lexicalDirectory);
10736
- if (directoryStat.isSymbolicLink()) {
10737
- throw new Error(`Cejel: refusing symlinked ingest directory: ${inputDirectory}`);
10738
- }
10739
- if (!directoryStat.isDirectory()) {
10740
- throw new Error(`Cejel: refusing non-directory ingest boundary: ${inputDirectory}`);
10741
- }
10742
- const candidateStat = lstatSync2(lexicalCandidate);
10743
- if (candidateStat.isSymbolicLink()) {
10744
- throw new Error(`Cejel: refusing symlinked ingest file: ${filePath}`);
10745
- }
10746
- if (!candidateStat.isFile()) {
10747
- throw new Error(`Cejel: refusing non-regular ingest file: ${filePath}`);
10748
- }
10749
- if (candidateStat.size > MAX_INGEST_DOCUMENT_BYTES) {
10750
- throw new Error(
10751
- `Cejel: ingest file exceeds the ${MAX_INGEST_DOCUMENT_BYTES.toLocaleString("en-US")} byte budget: ${filePath}`
10752
- );
10753
- }
10754
- const resolvedDirectory = realpathSync2(lexicalDirectory);
10755
- const resolvedCandidate = realpathSync2(lexicalCandidate);
10756
- if (!isContainedPath(resolvedCandidate, resolvedDirectory)) {
10757
- throw new Error(`Cejel: refusing ingest file outside its resolved input directory: ${filePath}`);
10758
- }
10759
- return resolvedCandidate;
10760
- }
10761
- function isResolvedIngestPathContained(candidate, directory) {
10762
- return isContainedPath(candidate, directory);
10763
- }
10764
-
10765
- // src/witan/generic-adapter.ts
10766
- var CEJEL_GENERIC_INGEST_CONTRACT_VERSION = "1.0";
13184
+ var CEJEL_GENERIC_INGEST_CONTRACT_VERSION = "1.1";
10767
13185
  var CEJEL_GENERIC_INGEST_CONTRACT_MAJOR = 1;
10768
13186
  var GenericFindingV1Schema = external_exports.object({
10769
13187
  ruleId: external_exports.string().min(1),
@@ -10781,6 +13199,17 @@ var GenericSignalDocumentV1Schema = external_exports.object({
10781
13199
  tool: external_exports.string().trim().min(1),
10782
13200
  signals: external_exports.array(GenericSignalV1Schema)
10783
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();
10784
13213
  function isGenericSignalDocument(raw) {
10785
13214
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return false;
10786
13215
  const doc = raw;
@@ -10808,8 +13237,10 @@ function assertSupportedContractVersion(version) {
10808
13237
  function formatValidationIssues(error) {
10809
13238
  return error.issues.map((issue) => `${issue.path.length > 0 ? issue.path.join(".") : "<document>"}: ${issue.message}`).join("; ");
10810
13239
  }
10811
- function parseGenericJson(raw) {
10812
- if (!isGenericSignalDocument(raw)) return [];
13240
+ function parseGenericDocument(raw) {
13241
+ if (!isGenericSignalDocument(raw)) {
13242
+ return { signals: [], identity: { producers: [], subjects: [] } };
13243
+ }
10813
13244
  assertSupportedContractVersion(raw.version);
10814
13245
  const parsed = GenericSignalDocumentV1Schema.safeParse(raw);
10815
13246
  if (!parsed.success) {
@@ -10817,7 +13248,23 @@ function parseGenericJson(raw) {
10817
13248
  `Cejel: invalid generic ingest contract v${raw.version}: ${formatValidationIssues(parsed.error)}`
10818
13249
  );
10819
13250
  }
10820
- 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) => ({
10821
13268
  source: parsed.data.tool,
10822
13269
  dimension: signal.dimension,
10823
13270
  weight: signal.weight,
@@ -10828,6 +13275,7 @@ function parseGenericJson(raw) {
10828
13275
  ...finding.location ? { location: finding.location } : {}
10829
13276
  }))
10830
13277
  }));
13278
+ return { signals, identity };
10831
13279
  }
10832
13280
 
10833
13281
  // src/witan/sarif-adapter.ts
@@ -11094,11 +13542,13 @@ function sanitizeIngestSource(source) {
11094
13542
  maxLength: MAX_INGEST_SOURCE_LENGTH
11095
13543
  });
11096
13544
  }
11097
- function parseIngestFile(filePath, options = {}) {
13545
+ function readIngestDocument(filePath, options = {}) {
11098
13546
  const resolvedFilePath = resolveIngestFilePath(filePath);
13547
+ const bytes = readFileSync4(resolvedFilePath);
13548
+ const sha256 = sha256OfBytes(bytes);
11099
13549
  let raw;
11100
13550
  try {
11101
- raw = JSON.parse(stripBom(readFileSync4(resolvedFilePath, "utf8")));
13551
+ raw = JSON.parse(stripBom(bytes.toString("utf8")));
11102
13552
  } catch (error) {
11103
13553
  const message = error instanceof Error ? error.message : String(error);
11104
13554
  throw new Error(`Cejel: could not parse ingest file as JSON: ${filePath} (${message})`);
@@ -11110,10 +13560,23 @@ function parseIngestFile(filePath, options = {}) {
11110
13560
  );
11111
13561
  }
11112
13562
  let signals;
11113
- if (looksLikeSarif(raw)) signals = parseSarifJson(raw, options.extraSarifDimensionRules);
11114
- else if (looksLikeScorecard(raw)) signals = parseScorecardJson(raw);
11115
- else if (isGenericSignalDocument(raw)) signals = parseGenericJson(raw);
11116
- 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) {
11117
13580
  throw new Error(
11118
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".`
11119
13582
  );
@@ -11125,12 +13588,17 @@ function parseIngestFile(filePath, options = {}) {
11125
13588
  `
11126
13589
  );
11127
13590
  }
11128
- return signals.map((signal) => ({
11129
- ...signal,
11130
- source: sanitizeIngestSource(signal.source),
11131
- provenance: options.provenance ?? "operator_supplied",
11132
- findings: signal.findings.map(clampFinding)
11133
- }));
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
+ };
11134
13602
  }
11135
13603
  function expandIngestPattern(pattern, maxMatches = Number.POSITIVE_INFINITY) {
11136
13604
  if (!pattern.includes("*")) return [pattern];
@@ -11214,6 +13682,7 @@ function discoverIngestInputs(repoPath, maxMatches = Number.POSITIVE_INFINITY) {
11214
13682
  // src/witan/public-scan.ts
11215
13683
  var MAX_INGEST_DOCUMENTS = 128;
11216
13684
  var MAX_RETAINED_INGEST_FINDINGS = 1e4;
13685
+ var MAX_INPUT_BINDING_RECORDS = 2 * MAX_INGEST_DOCUMENTS;
11217
13686
  function scoreRepoWithPublicCejel(options) {
11218
13687
  if (options.rubricVersion !== void 0) {
11219
13688
  assertSelectableRubricVersion(options.rubricVersion);
@@ -11225,35 +13694,90 @@ function scoreRepoWithPublicCejel(options) {
11225
13694
  ...options.generatedAt ? { generatedAt: options.generatedAt } : {},
11226
13695
  ...options.rubricVersion ? { rubricVersion: options.rubricVersion } : {}
11227
13696
  });
11228
- 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);
11229
13705
  const scored = createWitanReport(
11230
13706
  input,
11231
13707
  inputSignals.length > 0 ? inputSignals : void 0,
11232
13708
  void 0,
11233
13709
  options.toolVersion
11234
13710
  );
11235
- const historyLimitations = detectHistoryLimitations(options.repoPath);
11236
- 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;
11237
13717
  if (report.verdict !== "insufficient_source" || !isWitanNoMeasurementAbstention(report)) {
11238
- return report;
13718
+ return assertReportVerifiable(report);
11239
13719
  }
11240
13720
  const contextualReason = explainNoMeasurementSourceCoverage(
11241
13721
  options.repoPath,
11242
13722
  report.rubricVersion
11243
13723
  );
11244
- return contextualReason ? {
11245
- ...report,
11246
- insufficientSourceReason: contextualReason
11247
- } : 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
+ };
11248
13766
  }
11249
- function resolvePublicIngestSignals(options) {
13767
+ function resolvePublicIngestInputs(options) {
11250
13768
  const seen = /* @__PURE__ */ new Set();
11251
13769
  const documents = [];
13770
+ const samePathRepeats = /* @__PURE__ */ new Map();
11252
13771
  const addDocument = (file, provenance) => {
11253
13772
  const resolved = resolveIngestFilePath(file);
11254
- 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
+ }
11255
13779
  seen.add(resolved);
11256
- documents.push({ file: resolved, provenance });
13780
+ documents.push({ file: resolved, displayPath: file, provenance });
11257
13781
  if (documents.length > MAX_INGEST_DOCUMENTS) {
11258
13782
  throw new Error(
11259
13783
  `Cejel: ${documents.length} unique ingest documents exceed the ${MAX_INGEST_DOCUMENTS} ingest document budget; refusing to produce a partial certificate.`
@@ -11279,12 +13803,16 @@ function resolvePublicIngestSignals(options) {
11279
13803
  }
11280
13804
  }
11281
13805
  const signals = [];
13806
+ const records = [];
13807
+ const readByPath = /* @__PURE__ */ new Map();
13808
+ const scoredDigests = /* @__PURE__ */ new Set();
11282
13809
  let retainedFindings = 0;
11283
13810
  for (const document of documents) {
11284
- const parsed = parseIngestFile(document.file, {
13811
+ const read = readIngestDocument(document.file, {
11285
13812
  maxFindingCandidates: MAX_RETAINED_INGEST_FINDINGS - retainedFindings,
11286
13813
  provenance: document.provenance
11287
13814
  });
13815
+ const parsed = read.signals;
11288
13816
  const documentFindings = parsed.reduce((count, signal) => count + signal.findings.length, 0);
11289
13817
  retainedFindings += documentFindings;
11290
13818
  if (retainedFindings > MAX_RETAINED_INGEST_FINDINGS) {
@@ -11293,8 +13821,30 @@ function resolvePublicIngestSignals(options) {
11293
13821
  );
11294
13822
  }
11295
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
+ });
11296
13846
  }
11297
- return signals;
13847
+ return { signals, documents: records };
11298
13848
  }
11299
13849
 
11300
13850
  // src/product-identity.ts
@@ -11493,6 +14043,8 @@ function runCejelScan(options) {
11493
14043
  // are third-party repositories and must opt in explicitly.
11494
14044
  autoDiscoverIngest: true,
11495
14045
  warnOnEmptyIngestMatch: options.warnOnEmptyIngestMatch,
14046
+ ...options.strictIngest ? { strictIngest: true } : {},
14047
+ ...options.subjectRepository ? { subjectRepository: options.subjectRepository } : {},
11496
14048
  ...options.toolVersion ? { toolVersion: options.toolVersion } : {}
11497
14049
  });
11498
14050
  const summary = buildWitanCliSummary(report);
@@ -11503,7 +14055,13 @@ export {
11503
14055
  WitanReportSchema,
11504
14056
  computeApplicableMeasuredCoverage,
11505
14057
  formatCoverageSummary,
14058
+ EXTERNAL_FINDINGS_DISPLAY_LIMIT,
11506
14059
  formatExternalSourceLine,
14060
+ inheritedFindingNote,
14061
+ importedAdjustmentSentence,
14062
+ inputBindingHeadline,
14063
+ describeInputBindingRecord,
14064
+ describeScannedSubject,
11507
14065
  WITAN_RUBRIC_VERSION_V22,
11508
14066
  assertSelectableRubricVersion,
11509
14067
  isProspectiveRubricVersion,
@@ -11523,6 +14081,9 @@ export {
11523
14081
  serializeWitanReport,
11524
14082
  createWitanAttestation,
11525
14083
  verifyWitanAttestationBinding,
14084
+ stripTerminalControls,
14085
+ verifyRecordedInputDigests,
14086
+ resolveIngestFilePath,
11526
14087
  deriveProductIdentity,
11527
14088
  runCejelScan
11528
14089
  };