@cejel/cejel 0.6.11 → 0.7.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -739,20 +739,31 @@ function isWitanHistoryDepthUnknownAbstention(report) {
739
739
  function isWitanNoGitHistoryAbstention(report) {
740
740
  return report.verdict === "insufficient_source" && report.insufficientSourceReason === noGitHistoryAbstentionReason(report.rubricVersion);
741
741
  }
742
- function isShallowRepository(repoPath) {
743
- const result = execGit(["rev-parse", "--is-shallow-repository"], { cwd: repoPath });
744
- return result.ok && result.stdout.trim() === "true";
745
- }
746
- function readHistoryDepth(repoPath) {
742
+ function readHistoryDepthAnswer(repoPath) {
747
743
  const result = execGit(["rev-parse", "--is-shallow-repository"], { cwd: repoPath });
748
744
  if (result.ok) {
749
745
  const answer = result.stdout.trim();
750
- if (answer === "true") return "shallow";
751
- if (answer === "false") return "complete";
746
+ if (answer === "true") return { depth: "shallow", answeredByGit: true };
747
+ if (answer === "false") return { depth: "complete", answeredByGit: true };
752
748
  }
749
+ const failure = result.ok ? { noAnswer: true } : isExpectedGitAbsence(result) ? {} : { failure: result.reason };
753
750
  const entry = findVisibleGitMetadata(repoPath);
754
- if (entry === void 0) return result.ok ? "unknown" : "not_a_repository";
755
- return historyDepthFromFiles(entry);
751
+ if (entry === void 0) {
752
+ return { depth: result.ok ? "unknown" : "not_a_repository", answeredByGit: false, ...failure };
753
+ }
754
+ return { depth: historyDepthFromFiles(entry), answeredByGit: false, ...failure };
755
+ }
756
+ function isShallowHistoryDepth(answer) {
757
+ return answer.depth === "shallow";
758
+ }
759
+ function isHistoryDepthUnsettledByFailure(answer) {
760
+ return answer.failure !== void 0 && answer.depth !== "shallow" && answer.depth !== "complete";
761
+ }
762
+ var HISTORY_DEPTH_NO_ANSWER = "Cejel: local git history-depth check gave no answer (git printed neither true nor false).";
763
+ function historyDepthFailureLimitation(answer) {
764
+ if (answer.failure === void 0 && answer.noAnswer !== true) return void 0;
765
+ const failed = answer.failure !== void 0 ? describeGitFailure(answer.failure, "history-depth check") : HISTORY_DEPTH_NO_ANSWER;
766
+ return answer.depth !== "shallow" && answer.depth !== "complete" ? `${failed} Cejel could not read the history depth from the repository's files either.` : `${failed} Cejel read the history depth from the repository's own shallow file, which is the file git reads to answer.`;
756
767
  }
757
768
  function isDefinitelyAbsent(error) {
758
769
  const code = typeof error === "object" && error !== null ? error.code : void 0;
@@ -820,9 +831,8 @@ function historyDepthAbstentionReason(depth, rubricVersion, v27Wording) {
820
831
  if (depth === "not_a_repository") return noGitHistoryAbstentionReason(rubricVersion);
821
832
  return void 0;
822
833
  }
823
- function detectHistoryLimitations(repoPath) {
824
- if (!isShallowRepository(repoPath)) return void 0;
825
- return [SHALLOW_HISTORY_V27_ABSTENTION];
834
+ function detectHistoryLimitations(repoPath, answer = readHistoryDepthAnswer(repoPath)) {
835
+ return answer.answeredByGit && answer.depth === "shallow" ? [SHALLOW_HISTORY_V27_ABSTENTION] : void 0;
826
836
  }
827
837
 
828
838
  // src/witan/abstention.ts
@@ -885,6 +895,172 @@ function witanHistoryAbstentionInstruction(label) {
885
895
  // src/witan/certificate-presentation.ts
886
896
  init_esm_shims();
887
897
 
898
+ // src/witan/path-limits.ts
899
+ init_esm_shims();
900
+
901
+ // src/witan/finding-limits.ts
902
+ init_esm_shims();
903
+ var RULE_ID_MAX = 200;
904
+ var MESSAGE_MAX = 500;
905
+ var LOCATION_MAX = 700;
906
+ function clamp(value, max) {
907
+ if (value.length <= max) return value;
908
+ const ellipsis = "...";
909
+ return `${value.slice(0, Math.max(max - ellipsis.length, 0))}${ellipsis}`;
910
+ }
911
+ function clampFinding(finding) {
912
+ return {
913
+ ...finding,
914
+ ruleId: clamp(finding.ruleId, RULE_ID_MAX),
915
+ message: clamp(finding.message, MESSAGE_MAX),
916
+ ...finding.location ? { location: clamp(finding.location, LOCATION_MAX) } : {}
917
+ };
918
+ }
919
+
920
+ // src/witan/path-limits.ts
921
+ var REPORT_PATH_MAX = 700;
922
+ var MARKER_PREFIX = "\0cejel:path-over-report-limit:";
923
+ var MARKER_SUFFIX = "\0";
924
+ var OVER_LIMIT_ANCHOR_LABEL = `Path longer than the report's ${REPORT_PATH_MAX}-character limit; not cited`;
925
+ var OVER_LIMIT_ANCHOR = Object.freeze({
926
+ kind: "repository",
927
+ label: OVER_LIMIT_ANCHOR_LABEL,
928
+ path: "."
929
+ });
930
+ function overReportPathLimit(path) {
931
+ return path !== void 0 && path.length > REPORT_PATH_MAX;
932
+ }
933
+ function markSignal(signal, markerFor) {
934
+ const marks = (signal.positiveEvidence ?? []).some(({ path }) => overReportPathLimit(path)) || (signal.findings ?? []).some(({ evidence }) => overReportPathLimit(evidence.path));
935
+ if (!marks) return signal;
936
+ const markPointer = (pointer) => pointer.path !== void 0 && overReportPathLimit(pointer.path) ? { ...pointer, path: markerFor(pointer.path) } : pointer;
937
+ return {
938
+ ...signal,
939
+ ...signal.positiveEvidence ? { positiveEvidence: signal.positiveEvidence.map(markPointer) } : {},
940
+ ...signal.findings ? {
941
+ findings: signal.findings.map(
942
+ (finding) => overReportPathLimit(finding.evidence.path) ? { ...finding, evidence: markPointer(finding.evidence) } : finding
943
+ )
944
+ } : {}
945
+ };
946
+ }
947
+ function markOverLimitPaths(input) {
948
+ const markers = /* @__PURE__ */ new Map();
949
+ const markerFor = (path) => {
950
+ let marker = markers.get(path);
951
+ if (marker === void 0) {
952
+ marker = `${MARKER_PREFIX}${markers.size}${MARKER_SUFFIX}`;
953
+ markers.set(path, marker);
954
+ }
955
+ return marker;
956
+ };
957
+ const signals = input.signals?.map((signal) => markSignal(signal, markerFor));
958
+ const signalsMarked = signals?.some((signal, index) => signal !== input.signals?.[index]) ?? false;
959
+ const droppedWithheldPaths = (input.withheldPaths ?? []).filter(({ path }) => overReportPathLimit(path));
960
+ if (!signalsMarked && droppedWithheldPaths.length === 0) return { input, droppedWithheldPaths };
961
+ return {
962
+ input: {
963
+ ...input,
964
+ ...signalsMarked ? { signals } : {},
965
+ ...droppedWithheldPaths.length > 0 ? { withheldPaths: input.withheldPaths?.filter(({ path }) => !overReportPathLimit(path)) } : {}
966
+ },
967
+ droppedWithheldPaths
968
+ };
969
+ }
970
+ function isMarked(pointer) {
971
+ return pointer.path?.startsWith(MARKER_PREFIX) === true;
972
+ }
973
+ function isCitablePointer(pointer) {
974
+ return !isMarked(pointer);
975
+ }
976
+ var NO_SHORTENING = "No path is shortened, since a shortened path would name a file that does not exist.";
977
+ function overLimitCitationDisclosure(count) {
978
+ const one = count === 1;
979
+ return `Cejel did not cite ${count} repository ${one ? "path" : "paths"} here because ${one ? "it is" : "they are"} longer than the report's ${REPORT_PATH_MAX}-character path limit. Every finding that cited ${one ? "it" : "one"} is kept, with that citation left out, and the score and status were computed with ${one ? "it" : "them"}. ${NO_SHORTENING}`;
980
+ }
981
+ var OVER_LIMIT_WITHHELD_MARK = "not listed in withheldPaths because";
982
+ function overLimitWithheldPathsDisclosure(count, signals) {
983
+ const one = count === 1;
984
+ const it = one ? "it" : "them";
985
+ const head = `${count} withheld repository ${one ? "path is" : "paths are"} ${OVER_LIMIT_WITHHELD_MARK} ${one ? "it is" : "they are"} longer than the report's ${REPORT_PATH_MAX}-character path limit; contentReadSummary still counts ${it}.`;
986
+ const named = signals.length > 0 ? ` Checks that would have read ${it}: ${signals.join(", ")}.` : "";
987
+ const counted = signals.length > 0 ? ` ${signals.length} ${signals.length === 1 ? "check" : "checks"} would have read ${it}.` : "";
988
+ const withNames = `${head}${named} ${NO_SHORTENING}`;
989
+ return withNames.length <= STATEMENT_MAX ? withNames : `${head}${counted} ${NO_SHORTENING}`;
990
+ }
991
+ function statesUnlistedWithheldPaths(criteria) {
992
+ return criteria.some(
993
+ ({ disclosures, notes }) => (disclosures ?? []).some((sentence) => sentence.includes(OVER_LIMIT_WITHHELD_MARK)) || notes?.includes(OVER_LIMIT_WITHHELD_MARK) === true
994
+ );
995
+ }
996
+ var DISCLOSURES_MAX = 4;
997
+ var STATEMENT_MAX = 1e3;
998
+ function overLimitCitationNote(count) {
999
+ const one = count === 1;
1000
+ return `${count} ${one ? "path" : "paths"} over the report's ${REPORT_PATH_MAX}-character limit ${one ? "is" : "are"} not cited; scoring counted ${one ? "it" : "them"}.`;
1001
+ }
1002
+ function overLimitWithheldPathsNote(count) {
1003
+ const one = count === 1;
1004
+ return `${count} withheld ${one ? "path is" : "paths are"} ${OVER_LIMIT_WITHHELD_MARK} ${one ? "it is" : "they are"} over the report's ${REPORT_PATH_MAX}-character limit.`;
1005
+ }
1006
+ function stateOn(criterion, statements) {
1007
+ const disclosures = criterion.disclosures ?? [];
1008
+ const room = Math.max(0, DISCLOSURES_MAX - disclosures.length);
1009
+ const disclosed = statements.slice(0, room).map(({ full }) => full);
1010
+ const rest = statements.slice(room);
1011
+ const withDisclosures = disclosed.length > 0 ? { ...criterion, disclosures: [...disclosures, ...disclosed] } : criterion;
1012
+ if (rest.length === 0) return withDisclosures;
1013
+ const join5 = (notes, sentences) => notes ? `${notes} ${sentences}` : sentences;
1014
+ const whole = join5(criterion.notes, rest.map(({ full }) => full).join(" "));
1015
+ if (whole.length <= STATEMENT_MAX) return { ...withDisclosures, notes: whole };
1016
+ const countOnly = rest.map(({ countOnly: sentence }) => sentence).join(" ");
1017
+ const counted = join5(criterion.notes, countOnly);
1018
+ if (counted.length <= STATEMENT_MAX) return { ...withDisclosures, notes: counted };
1019
+ const kept = clamp(criterion.notes, STATEMENT_MAX - countOnly.length - 1);
1020
+ return { ...withDisclosures, notes: `${kept} ${countOnly}` };
1021
+ }
1022
+ function dropMarkedCitations(criteria, droppedWithheldPaths = []) {
1023
+ const statements = criteria.map(() => []);
1024
+ const cited = criteria.map((criterion, index) => {
1025
+ const dropped = /* @__PURE__ */ new Set();
1026
+ const evidence = criterion.evidence.filter((pointer) => {
1027
+ if (!isMarked(pointer)) return true;
1028
+ dropped.add(pointer.path);
1029
+ return false;
1030
+ });
1031
+ const findings = criterion.findings.map((finding) => {
1032
+ if (!isMarked(finding.evidence)) return finding;
1033
+ dropped.add(finding.evidence.path);
1034
+ return { ...finding, evidence: { ...OVER_LIMIT_ANCHOR } };
1035
+ });
1036
+ if (dropped.size === 0) return criterion;
1037
+ statements[index]?.push({
1038
+ full: overLimitCitationDisclosure(dropped.size),
1039
+ countOnly: overLimitCitationNote(dropped.size)
1040
+ });
1041
+ return { ...criterion, evidence, findings };
1042
+ });
1043
+ if (droppedWithheldPaths.length > 0) {
1044
+ if (cited.length === 0) {
1045
+ throw new Error("Cejel invariant: a report with withheld paths has at least one criterion");
1046
+ }
1047
+ const signals = [...new Set(droppedWithheldPaths.flatMap(({ signalsAdmitting }) => signalsAdmitting))].sort();
1048
+ const admitting = new Set(signals.map((signal) => signal.split(".")[0]));
1049
+ const index = Math.max(
1050
+ 0,
1051
+ cited.findIndex(({ id }) => admitting.has(id))
1052
+ );
1053
+ statements[index]?.push({
1054
+ full: overLimitWithheldPathsDisclosure(droppedWithheldPaths.length, signals),
1055
+ countOnly: overLimitWithheldPathsNote(droppedWithheldPaths.length)
1056
+ });
1057
+ }
1058
+ return cited.map((criterion, index) => {
1059
+ const own = statements[index] ?? [];
1060
+ return own.length === 0 ? criterion : stateOn(criterion, own);
1061
+ });
1062
+ }
1063
+
888
1064
  // src/witan/repo-signals.ts
889
1065
  init_esm_shims();
890
1066
  import { createHash } from "crypto";
@@ -979,6 +1155,51 @@ function isShellScriptPath(text) {
979
1155
  function execOnTerminatorFreeLine(pattern, line) {
980
1156
  return LINE_TERMINATOR_PATTERN.test(line) ? null : pattern.exec(line);
981
1157
  }
1158
+ function firstLineStart(text, from, to) {
1159
+ for (let index = from; index < to; index += 1) {
1160
+ if (index === 0 || LINE_TERMINATOR_PATTERN.test(text[index - 1] ?? "")) return index;
1161
+ }
1162
+ return to;
1163
+ }
1164
+ function stripLineLedHashCommentLines(text) {
1165
+ const pattern = /^[^\S\n\r\u2028\u2029]*#.*$/gm;
1166
+ let result = "";
1167
+ let kept = 0;
1168
+ for (let match = pattern.exec(text); match !== null; match = pattern.exec(text)) {
1169
+ let blankStart = match.index;
1170
+ while (blankStart > kept && WHITESPACE_PATTERN.test(text[blankStart - 1] ?? "")) blankStart -= 1;
1171
+ result += text.slice(kept, firstLineStart(text, blankStart, match.index));
1172
+ kept = match.index + match[0].length;
1173
+ }
1174
+ return result + text.slice(kept);
1175
+ }
1176
+ function widenToLeadingBlankRun(match, text) {
1177
+ if (match === null || text[match.index] === "{" || text[match.index] === ",") return match;
1178
+ const end = match.index + match[0].length;
1179
+ let blankStart = match.index;
1180
+ while (blankStart > 0 && WHITESPACE_PATTERN.test(text[blankStart - 1] ?? "")) blankStart -= 1;
1181
+ const before = text[blankStart - 1];
1182
+ const start = before === "{" || before === "," ? blankStart - 1 : firstLineStart(text, blankStart, match.index);
1183
+ match[0] = text.slice(start, end);
1184
+ match.index = start;
1185
+ return match;
1186
+ }
1187
+ var IDENTIFIER_START_PATTERN = /[A-Za-z_$]/;
1188
+ var IDENTIFIER_CHARACTER_PATTERN = /[\w$.-]/;
1189
+ function execFromIdentifierRunStarts(sticky, text, from) {
1190
+ let runHasStart = false;
1191
+ for (let index = from; index < text.length; index += 1) {
1192
+ const character = text[index] ?? "";
1193
+ if (character === '"' || character === "'" || !runHasStart && IDENTIFIER_START_PATTERN.test(character)) {
1194
+ sticky.lastIndex = index;
1195
+ const match = sticky.exec(text);
1196
+ if (match !== null) return match;
1197
+ }
1198
+ if (!IDENTIFIER_CHARACTER_PATTERN.test(character)) runHasStart = false;
1199
+ else if (IDENTIFIER_START_PATTERN.test(character)) runHasStart = true;
1200
+ }
1201
+ return null;
1202
+ }
982
1203
  var VERSION_OPERATOR_CHARACTERS = /* @__PURE__ */ new Set([">", "<", "=", "~", "!", "^"]);
983
1204
  var VERSION_DIGIT_PATTERN = /[\d.]/;
984
1205
  function firstVersionConstraint(text) {
@@ -1308,7 +1529,8 @@ function buildWitanInputFromRepoUntracked(options) {
1308
1529
  const usesV26SettlingEvidence = rubricVersion === WITAN_RUBRIC_VERSION_V26;
1309
1530
  const usesV26BuildStepEvidence = rubricVersion === WITAN_RUBRIC_VERSION_V26;
1310
1531
  const usesV27Closure = usesV27FalseAssuranceClosure(rubricVersion);
1311
- const unreadHistory = unreadHistoryDepth(readHistoryDepth(options.repoPath));
1532
+ const historyDepth = options.historyDepth ?? readHistoryDepthAnswer(options.repoPath);
1533
+ const unreadHistory = unreadHistoryDepth(historyDepth.depth);
1312
1534
  const structuralArchetype = classifyRepoArchetype(inventoryFiles, rubricVersion);
1313
1535
  const readableArchetype = rubricVersion === WITAN_RUBRIC_VERSION_V13 || rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15 || rubricVersion === WITAN_RUBRIC_VERSION_V16 || usesV17DetectorClosure(rubricVersion) ? applyReadableSourceRepresentationGate(
1314
1536
  options.repoPath,
@@ -1330,6 +1552,8 @@ function buildWitanInputFromRepoUntracked(options) {
1330
1552
  const usesV47Detectors = rubricVersion === WITAN_RUBRIC_VERSION_V15 || rubricVersion === WITAN_RUBRIC_VERSION_V16 || usesV17DetectorClosure(rubricVersion);
1331
1553
  const reviewableSourceProof = usesV17Detectors ? buildReviewableSourceProof(options.repoPath, inventoryFiles, headSha, rubricVersion) : void 0;
1332
1554
  const scanLimitations = new Set(fileInventory.scanLimitations);
1555
+ const historyDepthLimitation = historyDepthFailureLimitation(historyDepth);
1556
+ if (historyDepthLimitation !== void 0) scanLimitations.add(historyDepthLimitation);
1333
1557
  const coreSignals = collectRepoSignals(
1334
1558
  options.repoPath,
1335
1559
  generatedAt,
@@ -1355,7 +1579,8 @@ function buildWitanInputFromRepoUntracked(options) {
1355
1579
  reviewableSourceProof,
1356
1580
  inventoryAbsenceContext,
1357
1581
  scanLimitations,
1358
- unreadHistory
1582
+ unreadHistory,
1583
+ historyDepth
1359
1584
  );
1360
1585
  const archetype = usesV17Detectors && reviewableSourceProof?.passes === true && isV17RescuableAbstention(gatedArchetype.abstentionKind) && coreSignals.some(isMeasuredCriterionSignal) ? {
1361
1586
  archetype: isLikelyMonorepo(inventoryFiles) ? "monorepo" : "source",
@@ -1626,7 +1851,7 @@ var SEMANTIC_AUXILIARY_PATH_PATTERN_V14 = /(^|\/)(?:locales?|i18n|l10n|translati
1626
1851
  var HTML_SOURCE_EXTENSION_PATTERN = /\.(?:html?|xhtml)$/i;
1627
1852
  var HTML_META_REFRESH_PATTERN = /<meta\b(?=[^>]*\bhttp-equiv\s*=\s*["']?\s*refresh\b)(?=[^>]*\bcontent\s*=\s*["'][^"']*\burl\s*=)[^>]*>/i;
1628
1853
  var RUBY_PACKAGE_RECIPE_CLASS_PATTERN = /\bclass\s+[A-Za-z0-9_:]+\s*<\s*(?:Formula|Cask)\b/;
1629
- var RUBY_PACKAGE_RECIPE_URL_PATTERN = /^\s*url\s+["'][^"']+["']/m;
1854
+ var RUBY_PACKAGE_RECIPE_URL_PATTERN = /^[^\S\n\r\u2028\u2029]*url\s+["'][^"']+["']/m;
1630
1855
  var RUBY_PACKAGE_RECIPE_PATH_PATTERN = /(^|\/)(?:Formula|Casks?)(\/|$)/;
1631
1856
  function selectProportionalRepresentativeSourceFiles(files, rankKey, limit) {
1632
1857
  const eligibleFiles = [...new Set(files)];
@@ -1846,7 +2071,7 @@ function isHtmlRedirectMetadataSource(repoPath, file) {
1846
2071
  function isRubyPackageRecipeMetadataSource(repoPath, file) {
1847
2072
  if (!/\.rb$/i.test(file) || !RUBY_PACKAGE_RECIPE_PATH_PATTERN.test(file)) return false;
1848
2073
  const contents = readSemanticMetadataPrefix(repoPath, file);
1849
- const operativeContents = contents?.replace(/^\s*#.*$/gm, "");
2074
+ const operativeContents = contents === null ? void 0 : stripLineLedHashCommentLines(contents);
1850
2075
  return operativeContents !== void 0 && RUBY_PACKAGE_RECIPE_CLASS_PATTERN.test(operativeContents) && RUBY_PACKAGE_RECIPE_URL_PATTERN.test(operativeContents);
1851
2076
  }
1852
2077
  function readSemanticMetadataPrefix(repoPath, file) {
@@ -1968,7 +2193,7 @@ function buildNotApplicableSignal(criterionId, reason) {
1968
2193
  notApplicable: true
1969
2194
  };
1970
2195
  }
1971
- 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(), unreadHistory) {
2196
+ 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(), unreadHistory, historyDepth = COMPLETE_HISTORY_DEPTH) {
1972
2197
  const signals = [];
1973
2198
  const mono = resolveMonorepoContext(repoPath, scanLimitations);
1974
2199
  const collectCriterion = (criterionId, collect) => withContentReadCriterion(criterionId, collect);
@@ -1983,7 +2208,8 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
1983
2208
  useV23StrictCoverageAndRecipeRunners,
1984
2209
  reviewableSourceProof,
1985
2210
  inventoryAbsenceContext,
1986
- useV27FalseAssuranceClosure
2211
+ useV27FalseAssuranceClosure,
2212
+ scanLimitations
1987
2213
  )
1988
2214
  );
1989
2215
  const a2Signal = collectCriterion(
@@ -2003,7 +2229,8 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
2003
2229
  scanLimitations,
2004
2230
  useV26SettlingEvidence,
2005
2231
  useV27FalseAssuranceClosure,
2006
- unreadHistory
2232
+ unreadHistory,
2233
+ historyDepth
2007
2234
  )
2008
2235
  );
2009
2236
  const a3Signal = collectCriterion(
@@ -2049,7 +2276,7 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
2049
2276
  );
2050
2277
  const b2Signal = collectCriterion(
2051
2278
  "B2",
2052
- () => collectB2PrTraceEvidence(repoPath, repoFiles, useV27FalseAssuranceClosure)
2279
+ () => collectB2PrTraceEvidence(repoPath, repoFiles, useV27FalseAssuranceClosure, historyDepth)
2053
2280
  );
2054
2281
  const b3Signal = collectCriterion(
2055
2282
  "B3",
@@ -2095,7 +2322,7 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
2095
2322
  }
2096
2323
  return signals;
2097
2324
  }
2098
- function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV23StrictCoverageAndRecipeRunners, reviewableSourceProof, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT, useV27FalseAssuranceClosure = false) {
2325
+ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV23StrictCoverageAndRecipeRunners, reviewableSourceProof, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT, useV27FalseAssuranceClosure = false, scanLimitations = /* @__PURE__ */ new Set()) {
2099
2326
  const evidence = [];
2100
2327
  const findings = [];
2101
2328
  const v17TestFiles = findConcreteTestFiles(
@@ -2107,7 +2334,7 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
2107
2334
  const allTestFiles = pytestSettings !== void 0 ? v17TestFiles.filter(
2108
2335
  (file) => !PYTEST_CONFTEST_PATTERN.test(file) || pytestSettings.forFile(file).pythonFiles !== void 0
2109
2336
  ) : v17TestFiles;
2110
- const runWideHooks = pytestSettings ? findPytestRunWideSkips(repoPath, repoFiles) : [];
2337
+ const runWideHooks = pytestSettings ? findPytestRunWideSkips(repoPath, repoFiles, scanLimitations) : [];
2111
2338
  const runWideSkips = runWideHooks.filter((skip) => skip.withheld === void 0);
2112
2339
  const hookWithheldFor = (skip, file) => {
2113
2340
  if (skip.withheld !== void 0) {
@@ -2609,7 +2836,7 @@ function buildA1AuthenticatedAbsenceSignal(repoPath, proof, inventoryCount, inve
2609
2836
  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.`
2610
2837
  };
2611
2838
  }
2612
- function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV23PemPrivateKeyGrammar, useV24SecretContentContext, useV18NativeRls, inventoryAbsenceContext, scanLimitations, useV26SettlingEvidence = false, useV27FalseAssuranceClosure = false, unreadHistory) {
2839
+ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV23PemPrivateKeyGrammar, useV24SecretContentContext, useV18NativeRls, inventoryAbsenceContext, scanLimitations, useV26SettlingEvidence = false, useV27FalseAssuranceClosure = false, unreadHistory, historyDepth = COMPLETE_HISTORY_DEPTH) {
2613
2840
  const abstainsOnIncompleteHistory = useV27FalseAssuranceClosure || unreadHistory !== void 0;
2614
2841
  const noGitHistory = unreadHistory === "not_a_repository";
2615
2842
  const evidence = [];
@@ -2724,7 +2951,8 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
2724
2951
  const historyIncompleteness = a2HistoryIncompleteness(
2725
2952
  repoPath,
2726
2953
  a2HistoryReads,
2727
- historySecretScan?.truncated === true
2954
+ historySecretScan?.truncated === true,
2955
+ historyDepth
2728
2956
  );
2729
2957
  const dbLayerImportFile = migrationFiles.length === 0 ? implFiles.slice(0, 30).find((f) => fileContains(repoPath, f, DB_CLIENT_PATTERN)) : void 0;
2730
2958
  const hasDataLayer = migrationFiles.length > 0 || dbLayerImportFile !== void 0;
@@ -3444,7 +3672,7 @@ function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, us
3444
3672
  const hasActiveHealthcheck = withContentReadSignal(
3445
3673
  "A3",
3446
3674
  "dockerfile_healthcheck",
3447
- () => fileContains(repoPath, runtimeContainer, /^\s*HEALTHCHECK\s+(?!NONE\b)/im)
3675
+ () => fileContains(repoPath, runtimeContainer, /^[^\S\n\r\u2028\u2029]*HEALTHCHECK\s+(?!NONE\b)/im)
3448
3676
  );
3449
3677
  if (!hasActiveHealthcheck) {
3450
3678
  findings.push({
@@ -4764,8 +4992,11 @@ function unreadHistoryA2Sentence(depth) {
4764
4992
  }
4765
4993
  return depth === "shallow" ? "This repository is a shallow clone, so history secret scanning read only the commits present (reason: shallow_history); the critical finding stands on what was read." : "Cejel could not read the history depth of this checkout, so history secret scanning may have read only part of it (reason: history_depth_unknown); the critical finding stands on what was read.";
4766
4994
  }
4767
- function a2HistoryIncompleteness(repoPath, reads, blobCapHit) {
4768
- const shallow = isShallowRepository(repoPath);
4995
+ function a2HistoryIncompleteness(repoPath, reads, blobCapHit, historyDepth) {
4996
+ if (isHistoryDepthUnsettledByFailure(historyDepth) && historyDepth.failure !== void 0) {
4997
+ reads.noteFailure(historyDepth.failure);
4998
+ }
4999
+ const shallow = isShallowHistoryDepth(historyDepth);
4769
5000
  const limitHits = [...reads.limitHits].sort();
4770
5001
  const controlCharacterSkip = reads.controlCharacterCandidate;
4771
5002
  const gitUnavailable = reads.gitAbsent && hasVisibleGitMetadata(repoPath);
@@ -4906,6 +5137,8 @@ function a2HistoryApplicabilityDisclosure(incompleteness) {
4906
5137
  }
4907
5138
  var 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. B2 is left out of the composite rather than scored on the commits present. Re-scan a full clone (for actions/checkout, set fetch-depth: 0).`;
4908
5139
  var B2_SHALLOW_RATIO_WITHHELD_NOTE = `B2 abstained from the pull-request merge ratio only: 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. B2 is scored on the CI workflows, pull-request template and review-gate record in the checked-out files, with the ratio left out of its score rather than scored on the commits present. Re-scan a full clone (for actions/checkout, set fetch-depth: 0).`;
5140
+ var B2_SHALLOW_CHECK_FAILED_NOTE = `B2 abstained: Cejel could not tell whether this clone is shallow, because git failed to answer and the repository's files did not settle it (the scan limitations name the failed read), and fewer than ${RECENT_COMMIT_WINDOW} commits are visible, so the pull-request merge ratio may be read over a truncated window. Under v27 B2 is not scored on that ratio; it stays in the composite at 0. Re-scan once the local git failure is resolved.`;
5141
+ var COMPLETE_HISTORY_DEPTH = { depth: "complete", answeredByGit: true };
4909
5142
  function shallowHistoryAbstention(criterionId, notes) {
4910
5143
  return {
4911
5144
  criterionId,
@@ -4916,7 +5149,7 @@ function shallowHistoryAbstention(criterionId, notes) {
4916
5149
  notes
4917
5150
  };
4918
5151
  }
4919
- function collectB2PrTraceEvidence(repoPath, repoFiles, useV27FalseAssuranceClosure = false) {
5152
+ function collectB2PrTraceEvidence(repoPath, repoFiles, useV27FalseAssuranceClosure = false, historyDepth = COMPLETE_HISTORY_DEPTH) {
4920
5153
  const workflows = repoFiles.filter(isCiWorkflow);
4921
5154
  const prTemplate = repoFiles.find(
4922
5155
  (file) => /(^|\/)(pull_request_template|PULL_REQUEST_TEMPLATE)(\.md$|\/[^/]+\.md$)/.test(file)
@@ -4958,12 +5191,19 @@ function collectB2PrTraceEvidence(repoPath, repoFiles, useV27FalseAssuranceClosu
4958
5191
  ];
4959
5192
  if (evidence.length === 0) return null;
4960
5193
  const prTracePrimitives = creditedWorkflowCount + (prTemplate ? 1 : 0) + (branchProtectionDoc ? 1 : 0);
4961
- const ratioWithheld = isShallowRepository(repoPath) && readRecentCommits(repoPath).length < RECENT_COMMIT_WINDOW;
5194
+ const ratioWithheld = isShallowHistoryDepth(historyDepth) && readRecentCommits(repoPath).length < RECENT_COMMIT_WINDOW;
5195
+ const shallowCheckDecidesRatio = useV27FalseAssuranceClosure && isHistoryDepthUnsettledByFailure(historyDepth) && readRecentCommits(repoPath).length < RECENT_COMMIT_WINDOW;
4962
5196
  const uncreditedDisclosures = uncredited.size > 0 ? {
4963
5197
  disclosures: [uncreditedCiWorkflowDisclosure(WITAN_B2_UNCREDITED_CI_WORKFLOW_PREFIX, uncredited)]
4964
5198
  } : {};
4965
5199
  const uncreditedNote = uncredited.size > 0 ? uncreditedCiWorkflowNote(uncredited, "a PR-trace CI workflow") : void 0;
4966
5200
  const withUncreditedNote = (note) => uncreditedNote ? `${note} ${uncreditedCiWorkflowNote(uncredited, "a PR-trace CI workflow", CRITERION_NOTES_MAX_LENGTH - note.length - 1)}` : note;
5201
+ if (shallowCheckDecidesRatio) {
5202
+ return {
5203
+ ...shallowHistoryAbstention("B2", withUncreditedNote(B2_SHALLOW_CHECK_FAILED_NOTE)),
5204
+ ...uncreditedDisclosures
5205
+ };
5206
+ }
4967
5207
  if (ratioWithheld && prTracePrimitives === 0) {
4968
5208
  return {
4969
5209
  ...shallowHistoryAbstention("B2", withUncreditedNote(B2_SHALLOW_HISTORY_NOTE)),
@@ -11344,6 +11584,8 @@ function readShellCommands(text, directory, addopts, visit, unknownDirectory, me
11344
11584
  let current = directory;
11345
11585
  let why = directory === null ? unknownDirectory : void 0;
11346
11586
  let exported = addopts;
11587
+ let exports = 0;
11588
+ let exportReachesOther = false;
11347
11589
  const joined = stripShellHeredocBodies(text).replace(/\\\r?\n/g, " ");
11348
11590
  for (const segment of splitShellCommandSegments(joined)) {
11349
11591
  const all = shellWords(segment.trim());
@@ -11371,7 +11613,7 @@ function readShellCommands(text, directory, addopts, visit, unknownDirectory, me
11371
11613
  for (const word of words.slice(1)) {
11372
11614
  if (word.startsWith(PYTEST_ADDOPTS_ASSIGNMENT)) {
11373
11615
  exported = shellWords(word.slice(PYTEST_ADDOPTS_ASSIGNMENT.length));
11374
- if (mentions !== void 0) mentions.read += 1;
11616
+ exports += 1;
11375
11617
  }
11376
11618
  }
11377
11619
  continue;
@@ -11392,8 +11634,23 @@ function readShellCommands(text, directory, addopts, visit, unknownDirectory, me
11392
11634
  current = null;
11393
11635
  continue;
11394
11636
  }
11637
+ if (mentions !== void 0 && makeCommandOf(words) === void 0 && !writesGithubEnvOnly(words)) {
11638
+ mentions.reachesOther = true;
11639
+ if (exports > 0) exportReachesOther = true;
11640
+ }
11395
11641
  visit(words, current, local, current === null ? why : void 0);
11396
11642
  }
11643
+ if (mentions !== void 0 && !exportReachesOther) mentions.read += exports;
11644
+ }
11645
+ function writesGithubEnvOnly(words) {
11646
+ const name = basename(words[0] ?? "");
11647
+ if (name !== "echo" && name !== "printf") return false;
11648
+ let names = false;
11649
+ for (const word of words) {
11650
+ if (word.includes("$(") || word.includes("`")) return false;
11651
+ if (word.includes("GITHUB_ENV")) names = true;
11652
+ }
11653
+ return names;
11397
11654
  }
11398
11655
  var PYTHON_INTERPRETER_PATTERN = /^(?:python(?:\d+(?:\.\d+)*)?|pypy\d*(?:\.\d+)?|py)(?:\.exe)?$/i;
11399
11656
  var PYTHON_INTERPRETER_VARIABLE_PATTERN = /^(?:\{envpython\}|\$[({]?[A-Za-z_]*PY[A-Za-z_]*[)}]?)$/i;
@@ -11641,7 +11898,7 @@ function yamlPlainScalar(value) {
11641
11898
  const comment = trailingCommentStart(value);
11642
11899
  return (comment === -1 ? value : value.slice(0, comment)).trim();
11643
11900
  }
11644
- function readGithubRunSteps(text) {
11901
+ function readGithubRunSteps(text, usesSteps) {
11645
11902
  const lines = readYamlLines(text);
11646
11903
  const raw = text.split(/\r?\n/);
11647
11904
  const steps = [];
@@ -11710,6 +11967,7 @@ function readGithubRunSteps(text) {
11710
11967
  const firstRootLine = lines.findIndex((line) => !line.blank);
11711
11968
  const root = factsOf(firstRootLine === -1 ? /* @__PURE__ */ new Map() : mappingKeys(firstRootLine, lines[firstRootLine]?.keyColumn ?? 0));
11712
11969
  const jobs = /* @__PURE__ */ new Map();
11970
+ const stepKeysAt = /* @__PURE__ */ new Map();
11713
11971
  const jobFacts = (header, column) => {
11714
11972
  let facts = jobs.get(header);
11715
11973
  if (facts === void 0) {
@@ -11722,21 +11980,35 @@ function readGithubRunSteps(text) {
11722
11980
  };
11723
11981
  for (let index = 0; index < lines.length; index += 1) {
11724
11982
  const line = lines[index];
11725
- if (!line || line.blank || line.key !== "run" || line.value === "") continue;
11983
+ if (!line || line.blank || line.key !== "run" && line.key !== "uses" || line.value === "") continue;
11726
11984
  const column = line.keyColumn;
11727
11985
  const stepStart = itemStart[index] ?? -1;
11728
11986
  if (stepStart === -1) continue;
11729
- const stepKeys = mappingKeys(stepStart, column);
11987
+ let stepKeys = stepKeysAt.get(stepStart);
11988
+ if (stepKeys === void 0) {
11989
+ stepKeys = mappingKeys(stepStart, column);
11990
+ stepKeysAt.set(stepStart, stepKeys);
11991
+ }
11730
11992
  const list = parent[stepStart] ?? -1;
11731
11993
  const jobHeader = list === -1 ? -1 : parent[list] ?? -1;
11732
11994
  const job = jobHeader === -1 ? { never: false } : jobFacts(jobHeader, lines[list]?.keyColumn ?? 0);
11733
11995
  if (isFalse(scalarAt(stepKeys.get("if"))) || job.never) continue;
11996
+ if (line.key === "uses") {
11997
+ if (!stepKeys.has("run")) {
11998
+ usesSteps?.push({
11999
+ line: index,
12000
+ addoptsKeyLines: [root.addoptsLine, job.addoptsLine, addoptsLineIn(stepKeys)].filter((at) => at !== void 0)
12001
+ });
12002
+ }
12003
+ continue;
12004
+ }
11734
12005
  const directoryValue = scalarAt(stepKeys.get("working-directory")) ?? job.directory ?? root.directory;
11735
12006
  const directory = directoryValue === void 0 ? "" : directoryValue.includes("${{") ? null : resolveCommandPath("", directoryValue);
11736
12007
  const stepAddopts = addoptsIn(stepKeys);
11737
12008
  const addoptsValue = stepAddopts ?? job.addopts ?? root.addopts;
11738
12009
  const addoptsLine = stepAddopts !== void 0 ? addoptsLineIn(stepKeys) : job.addopts !== void 0 ? job.addoptsLine : root.addoptsLine;
11739
12010
  let command;
12011
+ let lastLine = index;
11740
12012
  if (/^[|>]/.test(line.value)) {
11741
12013
  const block = [];
11742
12014
  for (let next = index + 1; next < raw.length; next += 1) {
@@ -11744,6 +12016,7 @@ function readGithubRunSteps(text) {
11744
12016
  if (text2.trim() !== "" && text2.length - text2.trimStart().length <= column) break;
11745
12017
  block.push(text2);
11746
12018
  }
12019
+ lastLine = index + block.length;
11747
12020
  let indent = Number.POSITIVE_INFINITY;
11748
12021
  for (const text2 of block) if (text2.trim() !== "") indent = Math.min(indent, text2.length - text2.trimStart().length);
11749
12022
  const dedented = block.map((text2) => text2.slice(Number.isFinite(indent) ? indent : 0));
@@ -11757,6 +12030,7 @@ function readGithubRunSteps(text) {
11757
12030
  command,
11758
12031
  directory,
11759
12032
  line: index,
12033
+ lastLine,
11760
12034
  addoptsKeyLines: [root.addoptsLine, job.addoptsLine, stepAddoptsLine].filter((at) => at !== void 0),
11761
12035
  ...directory === null && directoryValue !== void 0 ? { unknownDirectory: `working-directory: ${directoryValue}` } : {},
11762
12036
  ...addoptsValue === void 0 || expression ? {} : { addopts: shellWords(addoptsValue) },
@@ -11778,9 +12052,13 @@ function readPytestInvocations(repoFiles, readText) {
11778
12052
  let hatchScripts;
11779
12053
  let hatchExpansionsLeft = HATCH_EXPANSION_BUDGET;
11780
12054
  const environmentTexts = [];
11781
- let addoptsMentionNotRead = false;
11782
- const noteAddoptsMentions = (text, read) => {
11783
- if (wordCount(text, "PYTEST_ADDOPTS") !== read) addoptsMentionNotRead = true;
12055
+ let addoptsMentionNotRead;
12056
+ const noteAddoptsMentions = (text, read, readLines, file) => {
12057
+ if (addoptsMentionNotRead !== void 0 || wordCount(text, "PYTEST_ADDOPTS") === read) return;
12058
+ const lines = text.split(/\r?\n/);
12059
+ let at = lines.findIndex((line, index) => !readLines.has(index) && containsWord(line, "PYTEST_ADDOPTS"));
12060
+ if (at === -1) at = lines.findIndex((line) => containsWord(line, "PYTEST_ADDOPTS"));
12061
+ addoptsMentionNotRead = { text: (lines[at] ?? "").trim(), source: a1NotePath(file), file, line: Math.max(at, 0) };
11784
12062
  };
11785
12063
  const makeEnvironments = [];
11786
12064
  let readingCi = true;
@@ -11883,23 +12161,47 @@ function readPytestInvocations(repoFiles, readText) {
11883
12161
  const writesAddopts = text.includes("GITHUB_ENV") && text.includes("PYTEST_ADDOPTS") ? "PYTEST_ADDOPTS written to $GITHUB_ENV" : void 0;
11884
12162
  const unreadWhy = writesAddopts ?? remote.checkoutPath;
11885
12163
  const keyLines = /* @__PURE__ */ new Set();
11886
- const shellMentions = writesAddopts === void 0 && text.includes("PYTEST_ADDOPTS") ? { read: 0 } : void 0;
11887
- for (const step of readGithubRunSteps(text)) {
12164
+ const keysReachingOther = /* @__PURE__ */ new Set();
12165
+ const readLines = /* @__PURE__ */ new Set();
12166
+ let fileReachesOther = false;
12167
+ let shellRead = 0;
12168
+ const mentioned = text.includes("PYTEST_ADDOPTS");
12169
+ const usesSteps = [];
12170
+ for (const step of readGithubRunSteps(text, usesSteps)) {
11888
12171
  for (const at of step.addoptsKeyLines) keyLines.add(at);
12172
+ const shellMentions = mentioned ? { read: 0, reachesOther: false } : void 0;
11889
12173
  const envAddopts = step.addopts !== void 0 && step.addoptsLine !== void 0 ? { words: step.addopts, line: step.addoptsLine } : void 0;
11890
12174
  const notKnownReason = writesAddopts ?? step.addoptsNotKnown;
11891
12175
  const envNotKnown = notKnownReason === void 0 ? void 0 : { reason: notKnownReason, source, file, line: writesAddopts === void 0 && step.addoptsLine !== void 0 ? step.addoptsLine : step.line };
11892
12176
  if (unreadWhy !== void 0) readShellCommands(step.command, null, step.addopts, visitor(source, file, step.line, 0, envAddopts, envNotKnown), unreadWhy, shellMentions);
11893
12177
  else readShellCommands(step.command, step.directory, step.addopts, visitor(source, file, step.line, 0, envAddopts, envNotKnown), step.unknownDirectory, shellMentions);
12178
+ if (shellMentions === void 0) continue;
12179
+ if (shellMentions.reachesOther) {
12180
+ fileReachesOther = true;
12181
+ for (const at of step.addoptsKeyLines) keysReachingOther.add(at);
12182
+ }
12183
+ if (writesAddopts !== void 0) continue;
12184
+ shellRead += shellMentions.read;
12185
+ if (!shellMentions.reachesOther && wordCount(step.command, "PYTEST_ADDOPTS") === shellMentions.read) {
12186
+ for (let at = step.line; at <= step.lastLine; at += 1) readLines.add(at);
12187
+ }
11894
12188
  }
11895
- if (text.includes("PYTEST_ADDOPTS")) {
11896
- let read = keyLines.size + (shellMentions?.read ?? 0);
11897
- if (writesAddopts !== void 0) {
12189
+ if (mentioned) {
12190
+ for (const step of usesSteps) {
12191
+ fileReachesOther = true;
12192
+ for (const at of step.addoptsKeyLines) keysReachingOther.add(at);
12193
+ }
12194
+ for (const at of keyLines) if (!keysReachingOther.has(at)) readLines.add(at);
12195
+ let read = shellRead;
12196
+ for (const at of keyLines) if (!keysReachingOther.has(at)) read += 1;
12197
+ if (writesAddopts !== void 0 && !fileReachesOther) {
11898
12198
  text.split(/\r?\n/).forEach((line, index) => {
11899
- if (!keyLines.has(index) && line.includes("GITHUB_ENV")) read += wordCount(line, "PYTEST_ADDOPTS");
12199
+ if (keyLines.has(index) || !line.includes("GITHUB_ENV")) return;
12200
+ read += wordCount(line, "PYTEST_ADDOPTS");
12201
+ readLines.add(index);
11900
12202
  });
11901
12203
  }
11902
- noteAddoptsMentions(text, read);
12204
+ noteAddoptsMentions(text, read, readLines, file);
11903
12205
  }
11904
12206
  for (const step of remote.pytestInputs) {
11905
12207
  invocations.push({ source, file, anchor: step.line, directory: "", args: [], text: step.uses, unread: step.uses });
@@ -11934,13 +12236,15 @@ function readPytestInvocations(repoFiles, readText) {
11934
12236
  environmentTexts.push(text);
11935
12237
  const jenkinsDir = file === "Jenkinsfile" ? jenkinsDirCall(text) : void 0;
11936
12238
  const addopts = readCiAddopts(text);
11937
- const shellMentions = text.includes("PYTEST_ADDOPTS") ? { read: 0 } : void 0;
12239
+ const shellMentions = text.includes("PYTEST_ADDOPTS") ? { read: 0, reachesOther: false } : void 0;
11938
12240
  for (const command of ciShellTexts(text)) {
11939
12241
  readShellCommands(command, jenkinsDir === void 0 ? "" : null, addopts, visitor(source, file, 0), jenkinsDir, shellMentions);
11940
12242
  }
11941
12243
  if (shellMentions !== void 0) {
11942
- const keyLine = text.split(/\r?\n/).find((line) => PYTEST_ADDOPTS_LINE_PATTERN.test(line));
11943
- noteAddoptsMentions(text, shellMentions.read + (keyLine !== void 0 && PYTEST_ADDOPTS_KEY_PATTERN.test(keyLine) ? 1 : 0));
12244
+ const lines = text.split(/\r?\n/);
12245
+ const keyAt = lines.findIndex((line) => PYTEST_ADDOPTS_LINE_PATTERN.test(line));
12246
+ const keyRead = keyAt !== -1 && PYTEST_ADDOPTS_KEY_PATTERN.test(lines[keyAt] ?? "") && !shellMentions.reachesOther;
12247
+ noteAddoptsMentions(text, shellMentions.read + (keyRead ? 1 : 0), new Set(keyRead ? [keyAt] : []), file);
11944
12248
  }
11945
12249
  }
11946
12250
  const noxAt = invocations.length;
@@ -11952,7 +12256,7 @@ function readPytestInvocations(repoFiles, readText) {
11952
12256
  const unreadPosargs = () => hidden ??= [...partitioned.unread, ...noxUnread, ...noxCommandsInUnreadFiles(repoFiles, readText, readActions)];
11953
12257
  const passedSets = [[], ...literal.map((entry) => entry.words)];
11954
12258
  const callCost = passedSets.reduce((sum, words) => sum + 1 + words.length, 0);
11955
- let noxLeft = NOX_POSARGS_BUDGET;
12259
+ let noxLeft = POSARGS_BUDGET;
11956
12260
  let unreadSaid = false;
11957
12261
  for (const file of usable.filter((candidate) => basename(candidate) === "noxfile.py")) {
11958
12262
  const text = readText(file);
@@ -12003,7 +12307,7 @@ function readPytestInvocations(repoFiles, readText) {
12003
12307
  const just = /[Jj]ustfile$/.test(file);
12004
12308
  const targets = just ? readJustRecipes(text) : readRecipeTargets(text);
12005
12309
  const headers = just ? new Map([...readJustRecipes(text)].map(([name, recipe]) => [name, recipe.line])) : makeTargetLines(text);
12006
- const setByEnvironment = (name) => name === "PYTEST_ADDOPTS" ? addoptsMentionNotRead : environmentTexts.some((ciText) => containsWord(ciText, name));
12310
+ const setByEnvironment = (name) => name === "PYTEST_ADDOPTS" ? addoptsMentionNotRead !== void 0 : environmentTexts.some((ciText) => containsWord(ciText, name));
12007
12311
  const justVariables = just ? readJustVariables(text) : void 0;
12008
12312
  const make = just ? void 0 : readMakeVariables(text, setByEnvironment);
12009
12313
  const budget = { left: MAKE_EXPANSION_BUDGET };
@@ -12013,6 +12317,7 @@ function readPytestInvocations(repoFiles, readText) {
12013
12317
  const expand = expandWith(make);
12014
12318
  let addoptsUnknown = false;
12015
12319
  let addoptsText;
12320
+ let addoptsNotKnownAt;
12016
12321
  if (justVariables !== void 0) {
12017
12322
  const exportsAll = /^set[ \t]+export(?:[ \t]*:=[ \t]*true)?[ \t]*$/m.test(text);
12018
12323
  const assigned = /^(?:export[ \t]+)?PYTEST_ADDOPTS[ \t]*:=/m.test(text);
@@ -12022,8 +12327,13 @@ function readPytestInvocations(repoFiles, readText) {
12022
12327
  addoptsUnknown = addoptsText === void 0;
12023
12328
  }
12024
12329
  } else {
12025
- const exported = /^(?:export[ \t]*|\.EXPORT_ALL_VARIABLES[ \t]*:.*|(?:override[ \t]+)?export[ \t]+PYTEST_ADDOPTS\b.*)$/m.test(text);
12026
- if (isUnknown("PYTEST_ADDOPTS")) addoptsUnknown = true;
12330
+ const exported = /^ *(?:export[ \t]*|\.EXPORT_ALL_VARIABLES[ \t]*:.*|(?:override[ \t]+)?export[ \t]+PYTEST_ADDOPTS\b.*)$/m.test(text);
12331
+ const assigns = make?.assigned.has("PYTEST_ADDOPTS") === true || make?.defaulted.has("PYTEST_ADDOPTS") === true;
12332
+ const mentionAt = exported || !assigns || makeOverrides.has("PYTEST_ADDOPTS") ? void 0 : addoptsMentionNotRead;
12333
+ if (mentionAt !== void 0) {
12334
+ addoptsUnknown = true;
12335
+ addoptsNotKnownAt = mentionAt;
12336
+ } else if (isUnknown("PYTEST_ADDOPTS")) addoptsUnknown = true;
12027
12337
  else if (exported && make?.values.has("PYTEST_ADDOPTS")) {
12028
12338
  const expanded = expand(make.values.get("PYTEST_ADDOPTS") ?? "");
12029
12339
  if (expanded.unknown) addoptsUnknown = true;
@@ -12033,7 +12343,8 @@ function readPytestInvocations(repoFiles, readText) {
12033
12343
  const addopts = addoptsText === void 0 ? void 0 : shellWords(shellWords(addoptsText.trim()).join(" "));
12034
12344
  const unknownRunsPytest = (command, expanded) => command.toLowerCase().includes("pytest") || expanded.toLowerCase().includes("pytest") || makeReferencesIn(expanded).some((name) => make?.pytestNames.has(name) === true);
12035
12345
  const firstTarget = [...targets.keys()][0];
12036
- const readRecipes = (names, variables, inForceAddopts, notKnown, addoptsAt) => {
12346
+ const readRecipes = (names, variables, inForceAddopts, notKnown, addoptsAt, notKnownAt) => {
12347
+ const pointAt = notKnown === void 0 ? void 0 : notKnownAt;
12037
12348
  const expandHere = variables === make ? expand : expandWith(variables);
12038
12349
  const reached = /* @__PURE__ */ new Set();
12039
12350
  const queue = names.filter((name) => name !== void 0 && targets.has(name));
@@ -12053,14 +12364,21 @@ function readPytestInvocations(repoFiles, readText) {
12053
12364
  }
12054
12365
  readShellCommands(expanded.text, repoDirectory(file), inForceAddopts, (words, directory, inForce, why) => {
12055
12366
  queue.push(...recipeTargetsNamed(words).filter((named) => targets.has(named)));
12056
- const visit = visitor(a1NotePath(file), file, headers.get(name) ?? 0, 0, void 0, void 0, addoptsAt);
12367
+ const visit = visitor(pointAt?.source ?? a1NotePath(file), pointAt?.file ?? file, pointAt?.line ?? headers.get(name) ?? 0, 0, void 0, void 0, addoptsAt);
12057
12368
  if (notKnown !== void 0) visit(words, null, inForce, notKnown);
12058
12369
  else visit(words, directory, inForce, why);
12059
12370
  });
12060
12371
  }
12061
12372
  }
12062
12373
  };
12063
- readRecipes([firstTarget, "test", "tests", "check", ...namedTargets], make, addopts, addoptsUnknown ? "PYTEST_ADDOPTS" : void 0);
12374
+ readRecipes(
12375
+ [firstTarget, "test", "tests", "check", ...namedTargets],
12376
+ make,
12377
+ addopts,
12378
+ addoptsUnknown ? addoptsNotKnownAt?.text ?? "PYTEST_ADDOPTS" : void 0,
12379
+ void 0,
12380
+ addoptsNotKnownAt
12381
+ );
12064
12382
  if (make === void 0 || addoptsUnknown) continue;
12065
12383
  const here = repoDirectory(file);
12066
12384
  const baseKey = addopts?.join("\0");
@@ -12093,11 +12411,12 @@ function readPytestInvocations(repoFiles, readText) {
12093
12411
  }
12094
12412
  const toxenv = readToxenvValues(environmentTexts);
12095
12413
  for (const name of toxenv.names) namedToxEnvironments.add(name);
12414
+ const toxBudget = { left: POSARGS_BUDGET, characters: POSARGS_CHARACTER_BUDGET };
12096
12415
  for (const file of usable.filter((candidate) => basename(candidate) === "tox.ini")) {
12097
12416
  const text = readText(file);
12098
12417
  if (text === void 0) continue;
12099
12418
  const notRead = (entry) => invocations.push({ source: entry.source, file: entry.file, anchor: entry.anchor, directory: "", args: [], text: entry.text, unread: entry.text });
12100
- readToxCommands(file, text, namedToxEnvironments, (anchor) => visitor(a1NotePath(file), file, anchor), toxPosargs, notRead, toxenv.unknown);
12419
+ readToxCommands(file, text, namedToxEnvironments, (anchor) => visitor(a1NotePath(file), file, anchor), toxPosargs, notRead, toxenv.unknown, toxBudget);
12101
12420
  }
12102
12421
  const after = invocations.splice(noxAt);
12103
12422
  for (const run of readNoxfiles()) invocations.push(run);
@@ -12364,6 +12683,8 @@ function readMakeVariables(text, setByEnvironment = () => false, environment) {
12364
12683
  const simple = /* @__PURE__ */ new Set();
12365
12684
  const unknown = /* @__PURE__ */ new Set();
12366
12685
  const pytestNames = /* @__PURE__ */ new Set();
12686
+ const assigned = /* @__PURE__ */ new Map();
12687
+ const defaulted = /* @__PURE__ */ new Set();
12367
12688
  const budget = { left: MAKE_EXPANSION_BUDGET };
12368
12689
  let fromEnvironment = environment !== void 0;
12369
12690
  if (environment !== void 0) values.set(environment.name, environment.value);
@@ -12373,7 +12694,9 @@ function readMakeVariables(text, setByEnvironment = () => false, environment) {
12373
12694
  };
12374
12695
  let conditional = 0;
12375
12696
  let defining;
12697
+ let lineIndex = -1;
12376
12698
  for (const line of text.split(/\r?\n/)) {
12699
+ lineIndex += 1;
12377
12700
  const trimmed = line.trim();
12378
12701
  if (defining !== void 0) {
12379
12702
  if (/^endef\b/.test(trimmed)) defining = void 0;
@@ -12381,6 +12704,7 @@ function readMakeVariables(text, setByEnvironment = () => false, environment) {
12381
12704
  continue;
12382
12705
  }
12383
12706
  if (line.startsWith(" ")) continue;
12707
+ const body = line.replace(/^[ \t]+/, "");
12384
12708
  const define = /^(?:override[ \t]+)?(?:export[ \t]+)?define[ \t]+([A-Za-z_][A-Za-z0-9_.-]*)/.exec(trimmed);
12385
12709
  if (define) {
12386
12710
  defining = define[1] ?? "";
@@ -12395,14 +12719,14 @@ function readMakeVariables(text, setByEnvironment = () => false, environment) {
12395
12719
  conditional = Math.max(0, conditional - 1);
12396
12720
  continue;
12397
12721
  }
12398
- const targetSpecific = /^[^#=:\t][^#=:]*:[ \t]*(?:(?:override|export|private)[ \t]+)*([A-Za-z_][A-Za-z0-9_.-]*)[ \t]*(?:\?=|:::=|::=|:=|\+=|!=|=)(.*)$/.exec(line);
12722
+ const targetSpecific = /^[^#=:\t][^#=:]*:[ \t]*(?:(?:override|export|private)[ \t]+)*([A-Za-z_][A-Za-z0-9_.-]*)[ \t]*(?:\?=|:::=|::=|:=|\+=|!=|=)(.*)$/.exec(body);
12399
12723
  if (targetSpecific) {
12400
12724
  const name2 = targetSpecific[1] ?? "";
12401
12725
  if ((targetSpecific[2] ?? "").toLowerCase().includes("pytest")) pytestNames.add(name2);
12402
12726
  forget(name2);
12403
12727
  continue;
12404
12728
  }
12405
- const match = /^(?:override[ \t]+)?(?:export[ \t]+)?([A-Za-z_][A-Za-z0-9_.-]*)[ \t]*(\?=|:::=|::=|:=|\+=|!=|=)(.*)$/.exec(line);
12729
+ const match = /^(?:override[ \t]+)?(?:export[ \t]+)?([A-Za-z_][A-Za-z0-9_.-]*)[ \t]*(\?=|:::=|::=|:=|\+=|!=|=)(.*)$/.exec(body);
12406
12730
  if (!match) continue;
12407
12731
  const name = match[1] ?? "";
12408
12732
  const operator = match[2] ?? "=";
@@ -12410,12 +12734,18 @@ function readMakeVariables(text, setByEnvironment = () => false, environment) {
12410
12734
  if (value.toLowerCase().includes("pytest")) pytestNames.add(name);
12411
12735
  if (unknown.has(name)) continue;
12412
12736
  const fromTheEnvironment = environment !== void 0 && name === environment.name;
12413
- if (fromTheEnvironment && environment.overrides && !/^override[ \t]/.test(line)) continue;
12414
- if (conditional > 0 || operator === "!=" || operator === ":::=" || operator === "?=" && setByEnvironment(name)) {
12737
+ if (fromTheEnvironment && environment.overrides && !/^override[ \t]/.test(body)) continue;
12738
+ if (conditional > 0 || operator === "!=" || operator === ":::=") {
12739
+ forget(name);
12740
+ continue;
12741
+ }
12742
+ if (operator === "?=" && setByEnvironment(name)) {
12743
+ defaulted.add(name);
12415
12744
  forget(name);
12416
12745
  continue;
12417
12746
  }
12418
12747
  if (fromTheEnvironment && operator !== "?=") fromEnvironment = false;
12748
+ if (operator !== "?=" && !assigned.has(name)) assigned.set(name, { text: line.trim(), line: lineIndex });
12419
12749
  if (operator === "?=") {
12420
12750
  if (!values.has(name)) {
12421
12751
  values.set(name, value);
@@ -12439,7 +12769,7 @@ function readMakeVariables(text, setByEnvironment = () => false, environment) {
12439
12769
  simple.delete(name);
12440
12770
  }
12441
12771
  }
12442
- return { values, unknown, pytestNames, fromEnvironment };
12772
+ return { values, unknown, pytestNames, fromEnvironment, assigned, defaulted };
12443
12773
  }
12444
12774
  function expandMakeVariables(command, values, isUnknown, budget, depth = 0) {
12445
12775
  if (!command.includes("$")) return { text: command, unknown: false };
@@ -12632,7 +12962,8 @@ function substituteHatchArguments(command, args) {
12632
12962
  return out + command.slice(index);
12633
12963
  }
12634
12964
  var NOX_POSARGS_WORD = "\0session.posargs";
12635
- var NOX_POSARGS_BUDGET = 4096;
12965
+ var POSARGS_BUDGET = 4096;
12966
+ var POSARGS_CHARACTER_BUDGET = 262144;
12636
12967
  function readNoxRunCalls(text) {
12637
12968
  const calls = [];
12638
12969
  const pattern = /\bsession\.run(?:_always)?[ \t]*\(/g;
@@ -12720,7 +13051,7 @@ function readCiAddopts(text) {
12720
13051
  }
12721
13052
  return void 0;
12722
13053
  }
12723
- function readToxCommands(file, text, named, visitAt, posargs = [], notRead = () => void 0, factorsUnknown = false) {
13054
+ function readToxCommands(file, text, named, visitAt, posargs = [], notRead = () => void 0, factorsUnknown = false, budget = { left: POSARGS_BUDGET, characters: POSARGS_CHARACTER_BUDGET }) {
12724
13055
  const sections = readIniSections(text);
12725
13056
  const toxDirectory = repoDirectory(file);
12726
13057
  const tox = sections.get("tox");
@@ -12740,6 +13071,12 @@ function readToxCommands(file, text, named, visitAt, posargs = [], notRead = ()
12740
13071
  if (section) environments.push({ section, names: [name] });
12741
13072
  }
12742
13073
  const { literal: literalPosargs, unread: unreadPosargs } = partitionPosargs(posargs);
13074
+ let posargsWords = 0;
13075
+ let posargsCharacters = 0;
13076
+ for (const entry of literalPosargs) {
13077
+ posargsWords += entry.words.length;
13078
+ for (const word of entry.words) posargsCharacters += word.length + 1;
13079
+ }
12743
13080
  let unreadSaid = false;
12744
13081
  const seen = /* @__PURE__ */ new Set();
12745
13082
  for (const { section, names } of environments) {
@@ -12766,6 +13103,16 @@ function readToxCommands(file, text, named, visitAt, posargs = [], notRead = ()
12766
13103
  const anchor = lineOfText.get(raw.trim()) ?? 0;
12767
13104
  const relative3 = directory === null ? "." : posix.relative(directory === "" ? "." : directory, toxDirectory === "" ? "." : toxDirectory) || ".";
12768
13105
  const substitute = (passed) => line.replace(/\{posargs\}/g, passed === void 0 ? "" : passed.map((word) => /\s/.test(word) ? `'${word}'` : word).join(" ")).replace(/\{posargs:([^}]*)\}/g, (_, fallback) => passed === void 0 ? fallback : passed.join(" ")).replace(/\{(?:toxinidir|tox_root)\}/g, relative3);
13106
+ if (line.includes("{posargs")) {
13107
+ let occurrences = 0;
13108
+ for (let at = line.indexOf("{posargs"); at !== -1; at = line.indexOf("{posargs", at + 1)) occurrences += 1;
13109
+ budget.left -= 1 + literalPosargs.length + occurrences * posargsWords;
13110
+ budget.characters -= occurrences * posargsCharacters;
13111
+ if (budget.left < 0 || budget.characters < 0) {
13112
+ notRead({ text: line, source: a1NotePath(file), file, anchor });
13113
+ continue;
13114
+ }
13115
+ }
12769
13116
  readShellCommands(substitute(void 0), directory, addopts, visitAt(anchor));
12770
13117
  if (!line.includes("{posargs")) continue;
12771
13118
  for (const entry of literalPosargs) readShellCommands(substitute(entry.words), directory, addopts, visitAt(anchor));
@@ -14038,10 +14385,15 @@ var PYTHON_BUILTIN_COPY_CALL_PATTERN = /^(?:list|tuple)[ \t]*\(/;
14038
14385
  var PYTHON_BUILTINS_REACH_PATTERN = /builtins|(?<![A-Za-z0-9_])(?:modules|__dict__|__globals__|__getattr__|__getattribute__|__setattr__|__delattr__|__import__|importlib|setattr|delattr|getattr|attrgetter|_getframe|currentframe|f_globals|f_locals|f_back|tb_frame|gi_frame|cr_frame|ag_frame|gc|ctypes|__subclasses__|pickle|marshal|runpy|__loader__|__spec__)(?![A-Za-z0-9_])|(?<![A-Za-z0-9_.])(?:exec|eval|compile|globals|vars|locals)(?![A-Za-z0-9_])|\.[ \t]*(?:list|tuple)(?![A-Za-z0-9_])(?![ \t]*\()/;
14039
14386
  var PYTHON_SOURCE_FILE_PATTERN = /\.(?:py|pyw|pyx|pxd|pxi)$/i;
14040
14387
  var PYTHON_UNREAD_MODULE_FILE_PATTERN = /\.(?:pyc|pyo|so|pyd|egg|whl|zip|pth)$/i;
14041
- function filesIncludingExcluded(repoPath) {
14388
+ function filesIncludingExcluded(repoPath, scanLimitations) {
14042
14389
  const tracked = readGitTrackedFileOutput(repoPath);
14043
14390
  if (tracked.ok) return tracked.stdout.split("\n").filter((line) => line !== "");
14044
- if (!isExpectedGitAbsence(tracked)) return void 0;
14391
+ if (!isExpectedGitAbsence(tracked)) {
14392
+ scanLimitations.add(
14393
+ `${describeGitFailure(tracked.reason, "full tracked-file inventory")} A1 credited no copy of pytest's collected items made through a builtin, because the files that may replace the builtin could not be listed.`
14394
+ );
14395
+ return void 0;
14396
+ }
14045
14397
  const files = [];
14046
14398
  const pending = [""];
14047
14399
  for (let next = pending.pop(); next !== void 0; next = pending.pop()) {
@@ -14060,8 +14412,8 @@ function filesIncludingExcluded(repoPath) {
14060
14412
  }
14061
14413
  return files;
14062
14414
  }
14063
- function builtinCopyReplacedBy(repoPath) {
14064
- const files = filesIncludingExcluded(repoPath);
14415
+ function builtinCopyReplacedBy(repoPath, scanLimitations) {
14416
+ const files = filesIncludingExcluded(repoPath, scanLimitations);
14065
14417
  if (files === void 0) return "and the repository's files could not be listed";
14066
14418
  for (const listed of files) {
14067
14419
  const quoted = listed.startsWith('"') && listed.endsWith('"') && listed.length > 1;
@@ -14398,7 +14750,7 @@ function readPytestCollectionHook(source, index, builtinsNamedIn = () => void 0)
14398
14750
  return void 0;
14399
14751
  }
14400
14752
  var A1_HOOK_NOT_READ = "hook not read: ";
14401
- function findPytestRunWideSkips(repoPath, repoFiles) {
14753
+ function findPytestRunWideSkips(repoPath, repoFiles, scanLimitations = /* @__PURE__ */ new Set()) {
14402
14754
  const found = [];
14403
14755
  let ignoresCollection = false;
14404
14756
  let builtinsRead = false;
@@ -14406,7 +14758,7 @@ function findPytestRunWideSkips(repoPath, repoFiles) {
14406
14758
  const builtinsNamedIn = () => {
14407
14759
  if (!builtinsRead) {
14408
14760
  builtinsRead = true;
14409
- builtinsFile = builtinCopyReplacedBy(repoPath);
14761
+ builtinsFile = builtinCopyReplacedBy(repoPath, scanLimitations);
14410
14762
  }
14411
14763
  return builtinsFile;
14412
14764
  };
@@ -15310,10 +15662,10 @@ function isRecord(value) {
15310
15662
  return typeof value === "object" && value !== null && !Array.isArray(value);
15311
15663
  }
15312
15664
  function isTestRunnerConfig(repoPath, file) {
15313
- return /(^|\/)(vitest|jest|pytest|playwright|cypress|mocha)\.config\.[cm]?[jt]s$/.test(file) || /(^|\/)(pytest\.ini|tox\.ini)$/.test(file) || /(^|\/)phpunit\.xml(\.dist)?$/.test(file) || /(^|\/)pyproject\.toml$/.test(file) && fileContains(repoPath, file, /^\[tool\.pytest(?:\.|\])/m) || /(^|\/)CMakeLists\.txt$/.test(file) && fileContains(repoPath, file, /\b(?:enable_testing|add_test|ctest_add_tests)\s*\(/i) || /(^|\/)Makefile$/.test(file) && fileContains(repoPath, file, /^\s*(?:test|tests|check)\s*:/m);
15665
+ return /(^|\/)(vitest|jest|pytest|playwright|cypress|mocha)\.config\.[cm]?[jt]s$/.test(file) || /(^|\/)(pytest\.ini|tox\.ini)$/.test(file) || /(^|\/)phpunit\.xml(\.dist)?$/.test(file) || /(^|\/)pyproject\.toml$/.test(file) && fileContains(repoPath, file, /^\[tool\.pytest(?:\.|\])/m) || /(^|\/)CMakeLists\.txt$/.test(file) && fileContains(repoPath, file, /\b(?:enable_testing|add_test|ctest_add_tests)\s*\(/i) || /(^|\/)Makefile$/.test(file) && fileContains(repoPath, file, /^[^\S\n\r\u2028\u2029]*(?:test|tests|check)\s*:/m);
15314
15666
  }
15315
15667
  function isRecipeTestEntry(repoPath, file) {
15316
- return isRecipeFile(file) && fileContains(repoPath, file, /^\s*(?:test|tests|check)\s*:/m);
15668
+ return isRecipeFile(file) && fileContains(repoPath, file, /^[^\S\n\r\u2028\u2029]*(?:test|tests|check)\s*:/m);
15317
15669
  }
15318
15670
  function findCoverageConfigFiles(repoPath, repoFiles, authoredOnly = false) {
15319
15671
  return repoFiles.filter(
@@ -16014,7 +16366,7 @@ function markdownClaimArtifactDeclarations(file, contents) {
16014
16366
  var WORKFLOW_WRITE_OPERATOR_PATTERN = />|(?:^|[\s|;&(])(?:tee|-o|--out[\w-]*)(?=[\s=]|$)/;
16015
16367
  var WORKFLOW_ARTIFACT_ACTION_PATTERN = /^\s*(?:-\s+)?uses\s*:\s*["']?actions\/(?:upload|download)-artifact(?:[/@]|["']?\s*$)/i;
16016
16368
  function workflowClaimArtifactDeclarations(contents) {
16017
- if (/^\s*(?:-\s+)?working-directory\s*:/m.test(contents)) return [];
16369
+ if (/^[^\S\n\r\u2028\u2029]*(?:-\s+)?working-directory\s*:/m.test(contents)) return [];
16018
16370
  const declarations = [];
16019
16371
  let stepsIndent = null;
16020
16372
  let stepItemIndent = null;
@@ -16093,13 +16445,13 @@ function isCoverageConfig(repoPath, file) {
16093
16445
  return true;
16094
16446
  }
16095
16447
  if (/(^|\/)(?:setup\.cfg|tox\.ini)$/.test(file)) {
16096
- return fileContains(repoPath, file, /^\s*\[coverage:(?:run|report)\]\s*$/m);
16448
+ return fileContains(repoPath, file, /^[^\S\n\r\u2028\u2029]*\[coverage:(?:run|report)\]\s*$/m);
16097
16449
  }
16098
16450
  if (/(^|\/)vite\.config\.[cm]?[jt]s$/.test(file)) {
16099
16451
  return viteConfigCollectsCoverage(repoPath, file);
16100
16452
  }
16101
16453
  if (/(^|\/)\.simplecov$/.test(file)) {
16102
- return fileContains(repoPath, file, /^(?!\s*#).*\bSimpleCov\b/m);
16454
+ return fileContains(repoPath, file, /^(?![^\S\n\r\u2028\u2029]*#).*\bSimpleCov\b/m);
16103
16455
  }
16104
16456
  if (/(^|\/)phpunit\.xml(?:\.dist)?$/.test(file)) {
16105
16457
  return /<coverage(?=[\s>/])/.test(stripXmlComments(readRepoText(join2(repoPath, file), "utf8")));
@@ -16144,7 +16496,10 @@ function packageJsonHasCoverageTooling(repoPath, file) {
16144
16496
  return packageJsonHasStaticCoverageTooling(repoPath, file);
16145
16497
  }
16146
16498
  function vitestCoveragePropertyCollects(contents) {
16147
- const property = /(?:^|[{,])\s*["']?coverage["']?\s*:\s*([^,\r\n}]*)/im.exec(contents)?.[1]?.trim();
16499
+ const property = widenToLeadingBlankRun(
16500
+ /(?:^|[{,])[^\S\n\r\u2028\u2029]*["']?coverage["']?\s*:\s*([^,\r\n}]*)/im.exec(contents),
16501
+ contents
16502
+ )?.[1]?.trim();
16148
16503
  if (!property || /^(?:false\b|null\b|undefined\b)/i.test(property)) return false;
16149
16504
  if (/^\{/.test(property) && /\benabled\s*:\s*false\b/i.test(contents)) return false;
16150
16505
  return true;
@@ -16158,7 +16513,10 @@ function viteConfigCollectsCoverage(repoPath, file) {
16158
16513
  if (!/["']\.\/vite\.config(?:\.[cm]?[jt]s)?["']/.test(vitestConfig)) return false;
16159
16514
  }
16160
16515
  const contents = stripJavaScriptComments(readRepoText(join2(repoPath, file), "utf8"));
16161
- const testProperty = /(?:^|[{,])\s*["']?test["']?\s*:\s*\{/m.exec(contents);
16516
+ const testProperty = widenToLeadingBlankRun(
16517
+ /(?:^|[{,])[^\S\n\r\u2028\u2029]*["']?test["']?\s*:\s*\{/m.exec(contents),
16518
+ contents
16519
+ );
16162
16520
  if (!testProperty) return false;
16163
16521
  const block = balancedBraceBlock(contents, testProperty.index + testProperty[0].length - 1);
16164
16522
  return vitestCoveragePropertyCollects(block);
@@ -16214,7 +16572,7 @@ function isV23CoverageConfig(repoPath, file) {
16214
16572
  }
16215
16573
  if (/(^|\/)jest\.config\.[cm]?[jt]s$/.test(file)) {
16216
16574
  const contents = stripJavaScriptComments(readRepoText(join2(repoPath, file), "utf8"));
16217
- return /(?:^|[{,])\s*["']?collectCoverage["']?\s*:\s*true\b/m.test(contents) || /(?:^|[{,])\s*["']?(?:coverageThreshold|coverageProvider|coverageDirectory)["']?\s*:/m.test(
16575
+ return /(?:^|[{,])[^\S\n\r\u2028\u2029]*["']?collectCoverage["']?\s*:\s*true\b/m.test(contents) || /(?:^|[{,])[^\S\n\r\u2028\u2029]*["']?(?:coverageThreshold|coverageProvider|coverageDirectory)["']?\s*:/m.test(
16218
16576
  contents
16219
16577
  );
16220
16578
  }
@@ -16228,7 +16586,10 @@ function coverageConfigurationDecision(repoPath, file) {
16228
16586
  if (/(^|\/)vitest\.config\.[cm]?[jt]s$/.test(file)) {
16229
16587
  const rawContents = readRepoText(join2(repoPath, file), "utf8");
16230
16588
  const contents = stripJavaScriptComments(rawContents);
16231
- const match = /(?:^|[{,])\s*["']?coverage["']?\s*:\s*([^,\r\n}]*)/im.exec(contents);
16589
+ const match = widenToLeadingBlankRun(
16590
+ /(?:^|[{,])[^\S\n\r\u2028\u2029]*["']?coverage["']?\s*:\s*([^,\r\n}]*)/im.exec(contents),
16591
+ contents
16592
+ );
16232
16593
  if (!match) return null;
16233
16594
  return {
16234
16595
  path: file,
@@ -16242,7 +16603,10 @@ function coverageConfigurationDecision(repoPath, file) {
16242
16603
  if (/(^|\/)jest\.config\.[cm]?[jt]s$/.test(file)) {
16243
16604
  const rawContents = readRepoText(join2(repoPath, file), "utf8");
16244
16605
  const contents = stripJavaScriptComments(rawContents);
16245
- const match = /(?:^|[{,])\s*["']?(collectCoverage|coverageThreshold|coverageProvider|coverageDirectory)["']?\s*:\s*([^,\r\n}]*)/m.exec(
16606
+ const match = widenToLeadingBlankRun(
16607
+ /(?:^|[{,])[^\S\n\r\u2028\u2029]*["']?(collectCoverage|coverageThreshold|coverageProvider|coverageDirectory)["']?\s*:\s*([^,\r\n}]*)/m.exec(
16608
+ contents
16609
+ ),
16246
16610
  contents
16247
16611
  );
16248
16612
  if (!match) return null;
@@ -17297,8 +17661,8 @@ var EXPRESS_ERROR_MIDDLEWARE_PATTERN = /\(\s*(?:_?err|_?error)(?:\s*:\s*(?:[^,()
17297
17661
  var EXPRESS_MIDDLEWARE_REACHABLE_PATTERN = /\.use\s*\(|\bmodule\.exports\b|\bexport\s+(?:default\b|function\b|const\b|class\b)/;
17298
17662
  var ERROR_BOUNDARY_FILENAME_PATTERN = /(^|\/)(?:error-boundary|error)\.(?:tsx|jsx|ts|js)$/;
17299
17663
  var RACK_SERVER_ENTRYPOINT_PATTERN = /Rack::(?:Server|Handler(?:::\w+)?)\.(?:start|run)\s*\(/;
17300
- var RACK_CONFIG_RUN_PATTERN = /^\s*run\s+(?:(?:[A-Z]\w*(?:::\w+)*(?:\.new)?|lambda)\b|->)/m;
17301
- var RUNTIME_CONTAINER_COMMAND_PATTERN = /^\s*(?:CMD|ENTRYPOINT)\s+.*(?:\b(?:start|serve|server|qgis|nginx|apache|gunicorn|uvicorn)\b|manage\.py\s+runserver).*$/im;
17664
+ var RACK_CONFIG_RUN_PATTERN = /^[^\S\n\r\u2028\u2029]*run\s+(?:(?:[A-Z]\w*(?:::\w+)*(?:\.new)?|lambda)\b|->)/m;
17665
+ var RUNTIME_CONTAINER_COMMAND_PATTERN = /^[^\S\n\r\u2028\u2029]*(?:CMD|ENTRYPOINT)\s+.*(?:\b(?:start|serve|server|qgis|nginx|apache|gunicorn|uvicorn)\b|manage\.py\s+runserver).*$/im;
17302
17666
  var NON_RUNTIME_CONTAINER_COMMAND_PATTERN = /\b(?:test|lint|build|compile|package|check)\b/i;
17303
17667
  var V20_NODE_RUNTIME_CONTAINER_COMMAND_PATTERN = /\bnode\b.*\b(?:main|server|app|service|index)\.(?:[cm]?js|ts)\b/i;
17304
17668
  var CI_DEPLOY_JOB_PATTERN = /(?:^\s{2,4}deploy\s*:|\b(?:fly|heroku|vercel|netlify|railway|render)\s+deploy\b|kubectl\s+apply\b|helm\s+(?:install|upgrade|deploy)\b|docker\s+push\b)/im;
@@ -18047,7 +18411,7 @@ function findFirstMatchingLineOutsideComments(repoPath, file, pattern) {
18047
18411
  const index = lines.findIndex((line) => pattern.test(line));
18048
18412
  return index === -1 ? null : index + 1;
18049
18413
  }
18050
- var PEM_PRIVATE_KEY_ASSIGNMENT_PATTERN = /(?:["'])?([A-Za-z_$][\w$.-]*)(?:["'])?\s*[:=]\s*(?:\|[-+0-9]*\s*\r?\n[ \t]*)?["'`]?-----BEGIN((?: (?:RSA|DSA|EC|OPENSSH|ENCRYPTED))? PRIVATE KEY)-----([\s\S]{0,32768}?)-----END\2-----/g;
18414
+ var PEM_PRIVATE_KEY_ASSIGNMENT_PATTERN = /(?:["'])?([A-Za-z_$][\w$.-]*)(?:["'])?\s*[:=]\s*(?:\|[-+0-9]*\s*\r?\n[ \t]*)?["'`]?-----BEGIN((?: (?:RSA|DSA|EC|OPENSSH|ENCRYPTED))? PRIVATE KEY)-----([\s\S]{0,32768}?)-----END\2-----/y;
18051
18415
  var BARE_PEM_PRIVATE_KEY_PATTERN = /["'`]?-----BEGIN((?: (?:RSA|DSA|EC|OPENSSH|ENCRYPTED))? PRIVATE KEY)-----([\s\S]{0,32768}?)-----END\1-----/g;
18052
18416
  var BARE_PEM_KEY_FILE_PATTERN = /(?:^|\/)id_(?:rsa|dsa|ecdsa|ed25519)$|\.(?:pem|key)$/i;
18053
18417
  function isBarePemKeyFile(file) {
@@ -18062,9 +18426,13 @@ function isPlausiblePemKeyBody(rawBody) {
18062
18426
  return !isPlaceholderSecretValue(normalized);
18063
18427
  }
18064
18428
  function scanForPemPrivateKeyMatch(pattern, contents, lines, hasIdentifierGroup) {
18065
- pattern.lastIndex = 0;
18429
+ const next = (from) => {
18430
+ if (pattern.sticky) return execFromIdentifierRunStarts(pattern, contents, from);
18431
+ pattern.lastIndex = from;
18432
+ return pattern.exec(contents);
18433
+ };
18066
18434
  let match;
18067
- while ((match = pattern.exec(contents)) !== null) {
18435
+ for (let from = 0; (match = next(from)) !== null; from = match.index + match[0].length) {
18068
18436
  const full = match[0];
18069
18437
  const identifier = hasIdentifierGroup ? match[1] ?? "" : "";
18070
18438
  const body = (hasIdentifierGroup ? match[3] : match[2]) ?? "";
@@ -19302,21 +19670,26 @@ init_esm_shims();
19302
19670
  init_esm_shims();
19303
19671
  import { createHash as createHash2 } from "crypto";
19304
19672
  var WITAN_RUBRIC_BEHAVIOUR_FINGERPRINTS = Object.freeze({
19305
- [WITAN_RUBRIC_VERSION_V17]: "sha256:5cd48f8c642300ae8736a09d4a24ce3a4f8885f6e099b5f27f99d950e0c944b4",
19306
- [WITAN_RUBRIC_VERSION_V18]: "sha256:4a5d17c84e265ec20cf54216d2e794dec1d299d859c69ac8ef1ab317fc479e2f",
19307
- [WITAN_RUBRIC_VERSION_V19]: "sha256:73dc0282a9ea2a481db8e86a9a5dc227a8b108356c358de029901f2dc38cef79",
19308
- [WITAN_RUBRIC_VERSION_V20]: "sha256:b5f4254a6056ff792d79a2d85892cdf05484eabded85ba13251a2c8fd5ac6336",
19309
- [WITAN_RUBRIC_VERSION_V21]: "sha256:f599d06354adb45af018639594cb6f44e7cb40445b84a0dcdd4438b8d7d069d0",
19310
- [WITAN_RUBRIC_VERSION_V22]: "sha256:34fb6ac5d1f6faf0912aa4d00924837eeee543614d7971247d005312b89928e1",
19311
- [WITAN_RUBRIC_VERSION_V23]: "sha256:ce24235861ce0f0dc4dafd22059a78d31f348e5c741e4056475867c29cc8b814",
19312
- [WITAN_RUBRIC_VERSION_V24]: "sha256:b221907d60d209cd3d57437892ae3dd873f4fa221ad8743954a30c8575d4e77c",
19313
- [WITAN_RUBRIC_VERSION_V25]: "sha256:0f2638a5088f95a610a0e12012442f1067af3e985b955f96ec71853d549693b9",
19673
+ // Re-pinned 2026-11-11 (#622), every entry: the corpus gained `history-shallow-depth-1` (a
19674
+ // depth-1 clone of a repository whose full clone scores A2 and B2 from history) and
19675
+ // `publish-after-check-workflow` (B3's check-before-publish shape, #620). No rubric's behaviour
19676
+ // changed: scored without those two specimens, the corpus reproduces every previous value
19677
+ // (rubric-behaviour-fingerprint.test.ts, "#622: the corpus extension ...").
19678
+ [WITAN_RUBRIC_VERSION_V17]: "sha256:1017cbaa7a77b4cdf50177bd20536480d874e191b46a1fe65cd7aaedc794a375",
19679
+ [WITAN_RUBRIC_VERSION_V18]: "sha256:d4479647b2ea561db907f2a0e890e1ea789aeb0ba93e8367a0c115df0084ba01",
19680
+ [WITAN_RUBRIC_VERSION_V19]: "sha256:afc007665ad29b348054699e560fb8b51ccfa9909964dd299a5eccf9156e7652",
19681
+ [WITAN_RUBRIC_VERSION_V20]: "sha256:2f81463b7b97591bfb5b22e25654d688947f19cc5a384f8b217a85a342475001",
19682
+ [WITAN_RUBRIC_VERSION_V21]: "sha256:4afda89876135bc742511257e17f8c0b6db8c88ae90818adfc60ff382b8a49da",
19683
+ [WITAN_RUBRIC_VERSION_V22]: "sha256:804ff598f785ef3a290d37db0617278f811755af49f9f14b1d68fa3d6299dfb2",
19684
+ [WITAN_RUBRIC_VERSION_V23]: "sha256:3d5a16189c49fc10d9c08e1f309f4966ba1c7a3d1dea86ec1e841f67752fab7e",
19685
+ [WITAN_RUBRIC_VERSION_V24]: "sha256:2b822b556a0ca073a0d7dc1ec7690a0b5b2b3e1581c00b03b4aebd987a4cbc0a",
19686
+ [WITAN_RUBRIC_VERSION_V25]: "sha256:86da3a1603ddff9635033721fa41029d7eea06c4b2e39ed8b1da5dd703f177a2",
19314
19687
  // v25's closure plus v26 item 6 (docs/calibration/v26/ruling-item-6.md and its amendment 1).
19315
19688
  // The corpus holds no declared claim-reconciliation artifact, so the v25 A5 finding no longer
19316
19689
  // fires on it and, by amendment 1, A5 is scored without reconciliation_artifact_depth. The A5
19317
19690
  // component and the report components of the specimens whose code-trust score that moves differ
19318
19691
  // from v25's. A2 does not move on this corpus.
19319
- [WITAN_RUBRIC_VERSION_V26]: "sha256:4530317d56fc73eb89287e344ad626e2b9a96908ef12eb9e9c13b839bd599dae",
19692
+ [WITAN_RUBRIC_VERSION_V26]: "sha256:0870e977698cf9fa3863c534007c8442fb9c61f274cbefa45fd724801bcc146f",
19320
19693
  // v17's closure plus the v27 false-assurance gate (#517-#520, #442, #527, #535). On this corpus
19321
19694
  // A4, B2 and B3 move against v17: specimens whose CI runs `npm ci` with no committed lockfile
19322
19695
  // score lockfile_coverage 0 under library norms, and `workspace-package-scripts`, whose only
@@ -19324,8 +19697,9 @@ var WITAN_RUBRIC_BEHAVIOUR_FINGERPRINTS = Object.freeze({
19324
19697
  // leaderboard/RUBRIC_CHANGELOG.md). That specimen's report component differs from v17's.
19325
19698
  // Re-pinned 2026-10-26 (#517, #520 follow-up): the specimen whose CI runs `npm ci` with no
19326
19699
  // committed lockfile now also carries A4's warning finding citing that line, so the A4 component
19327
- // moves; no score, status or report component does. B3's publishing rule reaches no specimen.
19328
- [WITAN_RUBRIC_VERSION_V27]: "sha256:dfe4e7e9ba37666f60b1ebcee4eb44917110798e9038c81eef079718db9ee7b1"
19700
+ // moves; no score, status or report component does. Until 2026-11-11 B3's publishing rule reached
19701
+ // no specimen; since #622, `publish-after-check-workflow` holds the shape it counts.
19702
+ [WITAN_RUBRIC_VERSION_V27]: "sha256:8fa9d87e3047afb80e8e09d08c5cb88a651c8e8b69b6b35184d2ab98ddd3a8fb"
19329
19703
  });
19330
19704
  function rubricBehaviourFingerprint(rubricVersion) {
19331
19705
  return WITAN_RUBRIC_BEHAVIOUR_FINGERPRINTS[rubricVersion];
@@ -19397,7 +19771,8 @@ function usesV17DetectorClosure2(rubricVersion) {
19397
19771
  }
19398
19772
  var MAX_SIGNAL_ADJUSTMENT = 0.8;
19399
19773
  function createWitanReport(input, inputSignals, rubric = WITAN_RUBRIC, toolVersion) {
19400
- const parsedInput = WitanReportInputSchema.parse(input);
19774
+ const marked = markOverLimitPaths(input);
19775
+ const parsedInput = WitanReportInputSchema.parse(marked.input);
19401
19776
  const signalsByCriterion = mergeSignalsByCriterion(parsedInput.signals);
19402
19777
  const readFailureAbstentions = new Set(
19403
19778
  [...signalsByCriterion.values()].filter((signal) => signal.insufficientData === true && !isCompositeExcludedAbstention(signal)).map((signal) => signal.criterionId)
@@ -19412,7 +19787,7 @@ function createWitanReport(input, inputSignals, rubric = WITAN_RUBRIC, toolVersi
19412
19787
  }
19413
19788
  }
19414
19789
  const consumedSignals = [];
19415
- const criteria = rubric.map((rubricCriterion) => {
19790
+ const scoredCriteria = rubric.map((rubricCriterion) => {
19416
19791
  const id = rubricCriterion.id;
19417
19792
  const signal = signalsByCriterion.get(id);
19418
19793
  const scored = scoreCriterion(signal, parsedInput.rubricVersion);
@@ -19462,6 +19837,7 @@ function createWitanReport(input, inputSignals, rubric = WITAN_RUBRIC, toolVersi
19462
19837
  ...signal?.disclosures ? { disclosures: signal.disclosures } : {}
19463
19838
  };
19464
19839
  });
19840
+ const criteria = dropMarkedCitations(scoredCriteria, marked.droppedWithheldPaths);
19465
19841
  const categoryOrder = [];
19466
19842
  for (const rubricCriterion of rubric) {
19467
19843
  if (!categoryOrder.includes(rubricCriterion.category)) {
@@ -20030,7 +20406,7 @@ function ensureFindingsExplainStatus(criterionId, score, status, findings, evide
20030
20406
  )) {
20031
20407
  return [...findings];
20032
20408
  }
20033
- const anchor = evidence[0] ?? {
20409
+ const anchor = evidence.find(isCitablePointer) ?? {
20034
20410
  kind: "repository",
20035
20411
  label: `${criterionId} metric-derived score`,
20036
20412
  path: "."
@@ -20659,11 +21035,13 @@ function describeWithheldReasons(entries) {
20659
21035
  function buildWithheldPathsGapSentence(report) {
20660
21036
  const withheldPaths = report.withheldPaths;
20661
21037
  if (withheldPaths === void 0) return void 0;
21038
+ const unlisted = statesUnlistedWithheldPaths(report.criteria);
20662
21039
  if (withheldPaths.length === 0) {
20663
- return "No content was withheld from any signal.";
21040
+ return unlisted ? "Content was withheld at a path longer than the report's 700-character path limit; withheldPaths does not list it, and a criterion's disclosures or notes say how much." : "No content was withheld from any signal.";
20664
21041
  }
20665
21042
  const count = withheldPaths.length;
20666
- const entryWord = count === 1 ? "content entry was" : "content entries were";
21043
+ const listed = unlisted ? "listed " : "";
21044
+ const entryWord = count === 1 ? `${listed}content entry was` : `${listed}content entries were`;
20667
21045
  const pronoun = count === 1 ? "it" : "them";
20668
21046
  const reasonLabel = describeWithheldReasons(withheldPaths);
20669
21047
  const intersecting = withheldPaths.filter((entry) => entry.signalsAdmitting.length > 0);
@@ -23417,25 +23795,6 @@ init_esm_shims();
23417
23795
  import { lstatSync as lstatSync4, opendirSync, readFileSync as readFileSync4, realpathSync as realpathSync4 } from "fs";
23418
23796
  import { join as join3, resolve as resolve4 } from "path";
23419
23797
 
23420
- // src/witan/finding-limits.ts
23421
- init_esm_shims();
23422
- var RULE_ID_MAX = 200;
23423
- var MESSAGE_MAX = 500;
23424
- var LOCATION_MAX = 700;
23425
- function clamp(value, max) {
23426
- if (value.length <= max) return value;
23427
- const ellipsis = "...";
23428
- return `${value.slice(0, Math.max(max - ellipsis.length, 0))}${ellipsis}`;
23429
- }
23430
- function clampFinding(finding) {
23431
- return {
23432
- ...finding,
23433
- ruleId: clamp(finding.ruleId, RULE_ID_MAX),
23434
- message: clamp(finding.message, MESSAGE_MAX),
23435
- ...finding.location ? { location: clamp(finding.location, LOCATION_MAX) } : {}
23436
- };
23437
- }
23438
-
23439
23798
  // src/witan/generic-adapter.ts
23440
23799
  init_esm_shims();
23441
23800
  import { readFileSync } from "fs";
@@ -23945,14 +24304,16 @@ function scoreRepoWithPublicCejel(options) {
23945
24304
  if (options.rubricVersion !== void 0) {
23946
24305
  assertSelectableRubricVersion(options.rubricVersion);
23947
24306
  }
24307
+ const historyDepth = readHistoryDepthAnswer(options.repoPath);
23948
24308
  const input = buildWitanInputFromRepo({
23949
24309
  productSlug: options.productSlug,
23950
24310
  productDisplayName: options.productDisplayName,
23951
24311
  repoPath: options.repoPath,
24312
+ historyDepth,
23952
24313
  ...options.generatedAt ? { generatedAt: options.generatedAt } : {},
23953
24314
  ...options.rubricVersion ? { rubricVersion: options.rubricVersion } : {}
23954
24315
  });
23955
- const scannedSubject = readScannedSubject(
24316
+ const { subject: scannedSubject, commitTimeLimitation } = readScannedSubjectAnswer(
23956
24317
  options.repoPath,
23957
24318
  input.repo.headSha,
23958
24319
  options.subjectRepository
@@ -23961,12 +24322,12 @@ function scoreRepoWithPublicCejel(options) {
23961
24322
  const inputBinding = documents.length > 0 ? buildInputBinding(documents, scannedSubject, options.strictIngest ? "strict" : "record-only") : void 0;
23962
24323
  if (options.strictIngest && inputBinding) assertStrictIngestBinding(documents, inputBinding);
23963
24324
  const scored = createWitanReport(
23964
- input,
24325
+ withScanLimitations(input, [documents.length > 0 ? commitTimeLimitation : void 0]),
23965
24326
  inputSignals.length > 0 ? inputSignals : void 0,
23966
24327
  void 0,
23967
24328
  options.toolVersion
23968
24329
  );
23969
- const historyLimitations = detectHistoryLimitations(options.repoPath);
24330
+ const historyLimitations = detectHistoryLimitations(options.repoPath, historyDepth);
23970
24331
  const withHistory = historyLimitations ? { ...scored, historyLimitations } : scored;
23971
24332
  const report = inputBinding ? { ...withHistory, inputBinding } : withHistory;
23972
24333
  if (report.verdict !== "insufficient_source" || !isWitanNoMeasurementAbstention(report)) {
@@ -23995,7 +24356,7 @@ function assertReportVerifiable(report) {
23995
24356
  ].join("\n")
23996
24357
  );
23997
24358
  }
23998
- function readScannedSubject(repoPath, headSha, subjectRepository) {
24359
+ function readScannedSubjectAnswer(repoPath, headSha, subjectRepository) {
23999
24360
  let repository;
24000
24361
  if (subjectRepository !== void 0) {
24001
24362
  repository = normalizeRepositoryIdentity(subjectRepository);
@@ -24006,19 +24367,34 @@ function readScannedSubject(repoPath, headSha, subjectRepository) {
24006
24367
  }
24007
24368
  }
24008
24369
  let commitTime;
24370
+ let commitTimeLimitation;
24009
24371
  if (headSha) {
24010
24372
  const result = execGit(["show", "-s", "--format=%ct", "HEAD"], { cwd: repoPath });
24373
+ if (!result.ok) {
24374
+ commitTimeLimitation = `${describeGitFailure(result.reason, "HEAD committer-time discovery")} Cejel recorded each ingested input's freshness as unverifiable.`;
24375
+ }
24011
24376
  const seconds = result.ok ? Number(result.stdout.trim()) : Number.NaN;
24012
24377
  if (Number.isSafeInteger(seconds) && seconds >= 0) {
24013
24378
  commitTime = plainIsoInstant(seconds * 1e3);
24014
24379
  }
24015
24380
  }
24016
24381
  return {
24017
- ...repository !== void 0 ? { repository } : {},
24018
- ...headSha ? { commit: headSha.toLowerCase() } : {},
24019
- ...commitTime !== void 0 ? { commitTime } : {}
24382
+ subject: {
24383
+ ...repository !== void 0 ? { repository } : {},
24384
+ ...headSha ? { commit: headSha.toLowerCase() } : {},
24385
+ ...commitTime !== void 0 ? { commitTime } : {}
24386
+ },
24387
+ ...commitTimeLimitation !== void 0 ? { commitTimeLimitation } : {}
24020
24388
  };
24021
24389
  }
24390
+ function withScanLimitations(input, extra) {
24391
+ const existing = input.scanLimitations ?? [];
24392
+ const added = [...new Set(extra.filter((entry) => entry !== void 0))].filter(
24393
+ (entry) => !existing.includes(entry)
24394
+ );
24395
+ if (added.length === 0) return input;
24396
+ return { ...input, scanLimitations: [...existing, ...added].slice(0, 16) };
24397
+ }
24022
24398
  function resolvePublicIngestInputs(options) {
24023
24399
  const seen = /* @__PURE__ */ new Set();
24024
24400
  const documents = [];