@cejel/cejel 0.6.10 → 0.6.11

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.
@@ -5042,7 +5042,9 @@ function collectB3CiDisciplineEvidence(repoPath, repoFiles, mono, useV27FalseAss
5042
5042
  uncreditedCiWorkflows(repoPath, workflows, inventory),
5043
5043
  publishingDetails,
5044
5044
  checkedPublishingDetails,
5045
- checkReading(repoPath, inventory)
5045
+ // Eighth pass (#659): v17's test-file list, the one A1 counts, read only when a runner is
5046
+ // given a positional word (testPathReader reads it once).
5047
+ checkReading(repoPath, inventory, () => findConcreteTestFiles(repoPath, repoFiles, WITAN_RUBRIC_VERSION_V17))
5046
5048
  );
5047
5049
  }) : /* @__PURE__ */ new Map();
5048
5050
  const uncreditedRootWorkflows = useV27FalseAssuranceClosure && mono ? withContentReadSignals(
@@ -6174,14 +6176,16 @@ function firstCheckLineByJob(contents, isGuardLine, reading, jobs, items, notCre
6174
6176
  const first = /* @__PURE__ */ new Map();
6175
6177
  const runSteps = githubActionsRunSteps(contents, isGuardLine);
6176
6178
  const environment = checkPathEnvironmentProblems(contents, jobs, items, runSteps);
6179
+ const uvSyncedJobs = /* @__PURE__ */ new Set();
6177
6180
  for (const step of runSteps) {
6181
+ const uvProjectSynced = uvSyncedJobs.has(step.job);
6182
+ const stepCommands = joinShellContinuationLines(step.lines.map(({ text }) => text)).flatMap((text) => shellCommands(text));
6183
+ if (stepCommands.some(runsProjectUvSync)) uvSyncedJobs.add(step.job);
6178
6184
  if (step.conditional || step.job === -1 || !step.ownKey) continue;
6179
- const commands = joinShellContinuationLines(step.lines.map(({ text }) => text)).flatMap(
6180
- (text) => shellCommands(text).map((words) => ({ words }))
6181
- );
6185
+ const commands = stepCommands.map((words) => ({ words }));
6182
6186
  if (stepShellIsConditional(step, commands)) continue;
6183
6187
  const changesDirectory = commands.some(({ words }) => ["cd", "pushd"].includes(commandHead(words).word));
6184
- const stepReading = changesDirectory ? { ...reading, ...ROOT_FILES_UNREAD } : reading;
6188
+ const stepReading = { ...changesDirectory ? { ...reading, ...ROOT_FILES_UNREAD } : reading, uvProjectSynced };
6185
6189
  const isCheck = (words) => isCheckCommand(words, stepReading);
6186
6190
  for (const { words } of commands) {
6187
6191
  const reason = unrecognisedCheckReason(words, stepReading) ?? checkLineReason(words, stepReading) ?? buildLineReason(words, stepReading) ?? seventhPassReason(words, stepReading);
@@ -6305,18 +6309,27 @@ var ROOT_FILES_UNREAD = {
6305
6309
  // Seventh pass (#641, item D): an install, and any npm, pnpm, yarn or bun command, reads the
6306
6310
  // manifest and configuration of the directory it runs in, which the root readings do not describe.
6307
6311
  installRunsNoProjectScript: false,
6308
- packageManagerConfigPlain: false
6312
+ packageManagerConfigPlain: false,
6313
+ // Eighth pass (#660): uv reads the pyproject.toml at or above the directory it runs in.
6314
+ uvProject: "unread"
6309
6315
  };
6310
6316
  var UNREAD_CHECK_READING = {
6311
6317
  ...ROOT_FILES_UNREAD,
6312
6318
  pathMayExist: () => true,
6313
- pythonModuleMayExist: () => true
6319
+ pythonModuleMayExist: () => true,
6320
+ testPath: () => "unread",
6321
+ uvProjectSynced: false
6314
6322
  };
6315
- function checkReading(root, inventory) {
6323
+ function checkReading(root, inventory, testFiles) {
6316
6324
  const tsconfig = rootTsconfigReading(root, inventory);
6317
6325
  const pythonModuleMayExist = pythonModuleReader(inventory);
6326
+ const testPath = testPathReader(testFiles);
6327
+ const uvProject = uvProjectReading(root, inventory);
6318
6328
  const tscAdmitted = Object.assign((words) => tscRunWritesNothingRead(words, tsconfig, inventory), {
6319
- moduleMayExist: (name) => pythonModuleMayExist(name, true)
6329
+ moduleMayExist: (name) => pythonModuleMayExist(name, true),
6330
+ // Eighth pass (#659, #660): a script or recipe runs from the root.
6331
+ testPath: (word, kind) => testPath(word, kind, true),
6332
+ uvRunBuildsProject: uvProject !== "no-build"
6320
6333
  });
6321
6334
  const testScript = rootTestScriptReading(root, inventory, tscAdmitted);
6322
6335
  const build = rootBuildScriptReading(root, inventory, tscAdmitted);
@@ -6337,7 +6350,10 @@ function checkReading(root, inventory) {
6337
6350
  pathMayExist: (name, fromRoot) => fromRoot ? inventory.entriesLower.has(name.toLowerCase()) : inventory.pathPartsLower.has(name.toLowerCase()),
6338
6351
  installRunsNoProjectScript: rootInstallRunsNoProjectScript(root, inventory),
6339
6352
  packageManagerConfigPlain: packageManagerConfigIsPlain(root, inventory),
6340
- pythonModuleMayExist
6353
+ pythonModuleMayExist,
6354
+ testPath,
6355
+ uvProject,
6356
+ uvProjectSynced: false
6341
6357
  };
6342
6358
  }
6343
6359
  var PYTHON_MODULE_FILE_PATTERN = /\.(?:py|pyc|pyw|so|pyd)$/;
@@ -7150,7 +7166,7 @@ function commandsAreAndChain(text) {
7150
7166
  const list = shellCommandList(text);
7151
7167
  return list.length > 0 && list.every(({ words, after }) => !commandIsBackgrounded(words) && (after.length === 0 || after.length === 1 && after[0] === "&&"));
7152
7168
  }
7153
- function peelCommandLaunchers(input) {
7169
+ function peelCommandLaunchers(input, admission) {
7154
7170
  const head = commandHead(input);
7155
7171
  if (head.inverted || commandIsBackgrounded(input)) return null;
7156
7172
  let words = [head.word, ...input.slice(head.argsFrom)];
@@ -7166,6 +7182,7 @@ function peelCommandLaunchers(input) {
7166
7182
  if (first === "env" || first === "cross-env" || first === "npx" || first === "pnpx" || first === "bunx") next = 1;
7167
7183
  else if (first === "pnpm" && second === "exec" || first === "bundle" && second === "exec") next = 2;
7168
7184
  else if (SCRIPT_ALIAS_TOOLS.has(first) && second === "run") return null;
7185
+ else if (first === "uv" && second === "run" && admission.uvRunBuildsProject) return null;
7169
7186
  else if (CHECK_COMMAND_RUN_LAUNCHERS.has(first) && second === "run") next = 2;
7170
7187
  else if (first === "coverage" && second === "run") {
7171
7188
  words = ["python", ...words.slice(2)];
@@ -7189,9 +7206,9 @@ function scriptCommandKind(words, tscAdmitted) {
7189
7206
  const python = (first === "python" || first === "python3") && second === "-m";
7190
7207
  if (python && tscAdmitted.moduleMayExist(third)) return "other";
7191
7208
  if (KNOWN_TEST_RUNNER_WORDS.has(first) || KNOWN_TEST_RUNNER_SUBCOMMANDS.get(first)?.includes(second)) {
7192
- return checkFormAdmits(words) === true ? "runner" : "other";
7209
+ return checkFormAdmits(words, tscAdmitted.testPath) === true ? "runner" : "other";
7193
7210
  }
7194
- if (python && KNOWN_TEST_RUNNER_MODULES.has(third)) return checkFormAdmits(words.slice(2)) === true ? "runner" : "other";
7211
+ if (python && KNOWN_TEST_RUNNER_MODULES.has(third)) return checkFormAdmits(words.slice(2), tscAdmitted.testPath) === true ? "runner" : "other";
7195
7212
  if (first === "tsc") return tscAdmitted(words) ? "check" : "other";
7196
7213
  if (PRE_RUNNER_CHECK_WORDS.has(first) || python && PRE_RUNNER_CHECK_WORDS.has(third)) return "check";
7197
7214
  return "other";
@@ -7208,7 +7225,7 @@ function packageScriptRun(words, scripts) {
7208
7225
  if (words.length === 2 && tool === "yarn" && Object.hasOwn(scripts, first)) return first;
7209
7226
  return void 0;
7210
7227
  }
7211
- function packageScriptCommands(scripts, name) {
7228
+ function packageScriptCommands(scripts, name, admission) {
7212
7229
  const commands = [];
7213
7230
  const visit = (script, depth, stack) => {
7214
7231
  if (depth > SCRIPT_EXPANSION_MAX_DEPTH || stack.has(script) || !Object.hasOwn(scripts, script)) return false;
@@ -7218,7 +7235,7 @@ function packageScriptCommands(scripts, name) {
7218
7235
  const body = scripts[part];
7219
7236
  if (typeof body !== "string" || !commandsAreAndChain(body)) return false;
7220
7237
  for (const words of shellCommands(body)) {
7221
- const peeled = peelCommandLaunchers(words);
7238
+ const peeled = peelCommandLaunchers(words, admission);
7222
7239
  const run = peeled === null ? void 0 : packageScriptRun(peeled, scripts);
7223
7240
  if (run !== void 0) {
7224
7241
  if (!visit(run, depth + 1, inner)) return false;
@@ -7240,7 +7257,7 @@ function rootPackageScripts(root, inventory) {
7240
7257
  function rootTestScriptReading(root, inventory, tscAdmitted) {
7241
7258
  const scripts = rootPackageScripts(root, inventory);
7242
7259
  if (scripts === void 0 || typeof scripts.test !== "string") return "unread";
7243
- const commands = packageScriptCommands(scripts, "test");
7260
+ const commands = packageScriptCommands(scripts, "test", tscAdmitted);
7244
7261
  const kinds = (commands ?? []).map((words) => words === null ? "other" : scriptCommandKind(words, tscAdmitted));
7245
7262
  return commands !== void 0 && kindsRunKnownRunner(kinds) ? "runner" : "unrecognised";
7246
7263
  }
@@ -7331,7 +7348,7 @@ function makeTargetCommandKinds(rules, target, mayBeUpToDate, tscAdmitted) {
7331
7348
  if (ignored || !commandsAreAndChain(text)) kinds.push("other");
7332
7349
  else {
7333
7350
  for (const words of shellCommands(text)) {
7334
- const peeled = peelCommandLaunchers(words);
7351
+ const peeled = peelCommandLaunchers(words, tscAdmitted);
7335
7352
  kinds.push(peeled === null ? "other" : scriptCommandKind(peeled, tscAdmitted));
7336
7353
  }
7337
7354
  }
@@ -7365,7 +7382,7 @@ function rootMakeTargetReadings(root, inventory, tscAdmitted) {
7365
7382
  function rootBuildScriptReading(root, inventory, tscAdmitted) {
7366
7383
  const scripts = rootPackageScripts(root, inventory);
7367
7384
  if (scripts === void 0 || !inventory.scanned.has("tsconfig.json")) return "other";
7368
- const commands = packageScriptCommands(scripts, "build");
7385
+ const commands = packageScriptCommands(scripts, "build", tscAdmitted);
7369
7386
  const runsOnlyTsc = commands !== void 0 && commands.length > 0 && commands.every((words) => words !== null && words[0] === "tsc" && !words.includes("--if-present"));
7370
7387
  if (!runsOnlyTsc) return "other";
7371
7388
  return commands.every((words) => words !== null && tscAdmitted(words)) ? "admitted" : "output-unread";
@@ -7439,6 +7456,7 @@ function isCheckCommand(input, reading, assignmentsRead = true, pathsRead = true
7439
7456
  if (read === void 0) return false;
7440
7457
  if (read.packageManager && !reading.packageManagerConfigPlain) return false;
7441
7458
  if (assignmentsRead && read.assignments.some((key) => !isInertEnvironmentKey(key))) return false;
7459
+ if (read.uv !== void 0 && uvRunProblem(read.uv, reading) !== void 0) return false;
7442
7460
  let own = read.words;
7443
7461
  if (read.alias !== void 0) {
7444
7462
  const alias = reading.scriptAlias(read.alias, read.words);
@@ -7458,7 +7476,7 @@ function isCheckCommand(input, reading, assignmentsRead = true, pathsRead = true
7458
7476
  if (NPM_FAMILY_TEST_SCRIPT_PATTERN.test(text)) {
7459
7477
  if (!reading.testScriptRuns) return false;
7460
7478
  if (packageTestArgumentsProblem(words, reading) !== void 0) return false;
7461
- } else if (checkFormAdmits(words) !== true) {
7479
+ } else if (checkFormAdmits(words, stepTestPath(reading)) !== true) {
7462
7480
  return false;
7463
7481
  }
7464
7482
  return verificationCommandPatterns().some((pattern) => {
@@ -7531,9 +7549,86 @@ function checkCommandWords(input) {
7531
7549
  } else if (CHECK_COMMAND_RUN_LAUNCHERS.has(launcher) && words[start + 1] === "run") {
7532
7550
  start += 2;
7533
7551
  if (SCRIPT_ALIAS_TOOLS.has(launcher)) return { words: words.slice(start), packageManager, assignments, alias: launcher };
7552
+ if (launcher === "uv") {
7553
+ let noProject = false;
7554
+ let noSync = false;
7555
+ let unread;
7556
+ for (; start < words.length; start += 1) {
7557
+ const word = words[start] ?? "";
7558
+ const option = word.split("=")[0] ?? word;
7559
+ if (word === "--no-project") noProject = true;
7560
+ else if (word === "--no-sync") noSync = true;
7561
+ else if (UV_RUN_PROJECT_OPTIONS.has(option)) {
7562
+ unread = option;
7563
+ if (!word.includes("=")) start += 1;
7564
+ } else break;
7565
+ }
7566
+ return { words: words.slice(start), packageManager, assignments, uv: { noProject, noSync, ...unread === void 0 ? {} : { unread } } };
7567
+ }
7534
7568
  }
7535
7569
  return { words: words.slice(start), packageManager, assignments };
7536
7570
  }
7571
+ var UV_RUN_PROJECT_OPTIONS = /* @__PURE__ */ new Set(["--package", "--project", "--directory"]);
7572
+ var UV_PROJECT_ADMITTED_KEYS = /* @__PURE__ */ new Set(["package", "dev-dependencies", "required-version", "index"]);
7573
+ function readUvProject(text) {
7574
+ let buildSystem = false;
7575
+ let packaged;
7576
+ const lines = text.split(/\r?\n/);
7577
+ let table = [];
7578
+ const uvKeyUnread = (path) => path[0] === "tool" && (path.length === 1 || path[1] === "uv" && (path.length === 2 || !UV_PROJECT_ADMITTED_KEYS.has(path[2] ?? "")));
7579
+ for (let index = 0; index < lines.length; index += 1) {
7580
+ const trimmed = (lines[index] ?? "").trim();
7581
+ if (trimmed === "" || trimmed.startsWith("#")) continue;
7582
+ const header = /^\[\[?([^\]]*)\]\]?/.exec(trimmed);
7583
+ if (header) {
7584
+ if ((header[1] ?? "").includes("\\")) return "unread";
7585
+ table = tomlKeyPath(header[1] ?? "");
7586
+ if (table[0] === "build-system") buildSystem = true;
7587
+ if (table[0] === "tool" && table[1] === "uv" && table.length > 2 && !UV_PROJECT_ADMITTED_KEYS.has(table[2] ?? "")) return "unread";
7588
+ continue;
7589
+ }
7590
+ const equals = tomlKeyEnd(trimmed);
7591
+ if (equals === -1) continue;
7592
+ const scanner = new TomlValueScanner();
7593
+ scanner.feed(trimmed.slice(equals + 1));
7594
+ while (!scanner.complete() && index + 1 < lines.length) {
7595
+ index += 1;
7596
+ scanner.feed(`
7597
+ ${lines[index] ?? ""}`);
7598
+ }
7599
+ if (trimmed.slice(0, equals).includes("\\")) return "unread";
7600
+ const path = [...table, ...tomlKeyPath(trimmed.slice(0, equals))];
7601
+ if (path[0] === "build-system") buildSystem = true;
7602
+ if (uvKeyUnread(path)) return "unread";
7603
+ if (path[0] === "tool" && path[1] === "uv" && path[2] === "package") {
7604
+ const code = scanner.code().trim();
7605
+ if (path.length !== 3 || code !== "true" && code !== "false") return "unread";
7606
+ packaged = code === "true";
7607
+ }
7608
+ }
7609
+ if (packaged === false) return "no-build";
7610
+ return packaged === true || buildSystem ? "builds" : "no-build";
7611
+ }
7612
+ function uvProjectReading(root, inventory) {
7613
+ if (inventory.hasUvConfiguration || !inventory.scanned.has("pyproject.toml")) return "unread";
7614
+ const text = readRepoText(join2(root, "pyproject.toml"), "utf8");
7615
+ return text === void 0 ? "unread" : readUvProject(text);
7616
+ }
7617
+ function uvRunProblem(uv, reading) {
7618
+ if (uv.unread !== void 0) return UV_RUN_UNREAD_REASON;
7619
+ if (uv.noProject) return void 0;
7620
+ if (reading.uvProject === "unread") return UV_RUN_UNREAD_REASON;
7621
+ if (reading.uvProject === "no-build") return void 0;
7622
+ if (!uv.noSync) return UV_INSTALLS_PROJECT_REASON;
7623
+ return reading.uvProjectSynced ? UV_SYNCED_PROJECT_REASON : void 0;
7624
+ }
7625
+ function runsProjectUvSync(input) {
7626
+ const head = commandHead(input);
7627
+ const words = [head.word, ...input.slice(head.argsFrom)];
7628
+ let start = 0;
7629
+ while (start < words.length && SHELL_ASSIGNMENT_PATTERN.test(words[start] ?? "")) start += 1;
7630
+ return words[start] === "uv" && words[start + 1] === "sync" && !words.includes("--no-install-project");
7631
+ }
7537
7632
  var NPM_TEST_KEPT_WORDS = /* @__PURE__ */ new Set(["--silent", "-s"]);
7538
7633
  var PACKAGE_SELECTING_FLAGS = /* @__PURE__ */ new Set([
7539
7634
  "--prefix",
@@ -7569,7 +7664,12 @@ function packageTestArgumentsProblem(words, reading) {
7569
7664
  if (last === void 0) return UNREAD_TEST_ARGUMENT_REASON;
7570
7665
  const runner = [...last, ...passed];
7571
7666
  const python = (runner[0] === "python" || runner[0] === "python3") && runner[1] === "-m";
7572
- return checkFormAdmits(python ? runner.slice(2) : runner) === true ? void 0 : RUNNER_ARGUMENT_REASON;
7667
+ const admitted = checkFormAdmits(python ? runner.slice(2) : runner, stepTestPath(reading));
7668
+ if (typeof admitted === "object") return positionalReason(admitted);
7669
+ return admitted === true ? void 0 : RUNNER_ARGUMENT_REASON;
7670
+ }
7671
+ function stepTestPath(reading) {
7672
+ return (word, kind) => reading.testPath(word, kind, reading.pipNamesFromRoot);
7573
7673
  }
7574
7674
  function checkLineReason(input, reading) {
7575
7675
  if (commandIsBackgrounded(input)) return void 0;
@@ -7627,6 +7727,10 @@ function seventhPassReason(input, reading) {
7627
7727
  if (read === void 0 || read.packageManager && !reading.packageManagerConfigPlain) return void 0;
7628
7728
  if (read.assignments.some((key) => !isInertEnvironmentKey(key))) return void 0;
7629
7729
  let words = read.words;
7730
+ if (read.uv !== void 0) {
7731
+ const problem = uvRunProblem(read.uv, reading);
7732
+ if (problem !== void 0) return words.length > 0 && !isPathCommandWord(words[0] ?? "") && namesCheckAtStart(words) ? problem : void 0;
7733
+ }
7630
7734
  if (read.alias !== void 0) {
7631
7735
  const alias = reading.scriptAlias(read.alias, words);
7632
7736
  if (alias.kind === "script") return alias.reading === "runner" ? void 0 : SCRIPT_ALIAS_SCRIPT_REASON(read.alias, alias.name);
@@ -7648,7 +7752,9 @@ function seventhPassReason(input, reading) {
7648
7752
  const first = (words[0] ?? "").toLowerCase();
7649
7753
  if (first === "make" || LINT_OR_FORMAT_COMMANDS.has(first) || NPM_FAMILY_TEST_SCRIPT_PATTERN.test(words.join(" "))) return void 0;
7650
7754
  if (words.some((word) => REPORT_ONLY_FLAGS.has(word))) return void 0;
7651
- return checkFormAdmits(words) === false ? RUNNER_ARGUMENT_REASON : void 0;
7755
+ const admitted = checkFormAdmits(words, stepTestPath(reading));
7756
+ if (typeof admitted === "object") return positionalReason(admitted);
7757
+ return admitted === false ? RUNNER_ARGUMENT_REASON : void 0;
7652
7758
  }
7653
7759
  var JUST_RECIPE_HEADER_PATTERN = /^@?([A-Za-z_][A-Za-z0-9_-]*)[ \t]*:(?!=)(.*)$/;
7654
7760
  var JUST_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_-]*$/;
@@ -7711,7 +7817,7 @@ function rootJustRecipeReader(root, inventory, tscAdmitted) {
7711
7817
  }
7712
7818
  if (ignored || !commandsAreAndChain(text)) kinds.push("other");
7713
7819
  else for (const words of shellCommands(text)) {
7714
- const peeled = peelCommandLaunchers(words);
7820
+ const peeled = peelCommandLaunchers(words, tscAdmitted);
7715
7821
  kinds.push(peeled === null ? "other" : scriptCommandKind(peeled, tscAdmitted));
7716
7822
  }
7717
7823
  if (kinds.length > SCRIPT_EXPANSION_MAX_COMMANDS) return false;
@@ -7777,7 +7883,7 @@ function rootToxReading(root, inventory, tscAdmitted) {
7777
7883
  for (const line of commands.split("\n").map((entry) => entry.trim()).filter((entry) => entry !== "")) {
7778
7884
  if (line.startsWith("-") || line.startsWith("!") || /[|&;<>$`()]/.test(line)) kinds.push("other");
7779
7885
  else {
7780
- const peeled = peelCommandLaunchers(shellWords(line));
7886
+ const peeled = peelCommandLaunchers(shellWords(line), tscAdmitted);
7781
7887
  kinds.push(peeled === null ? "other" : scriptCommandKind(peeled, tscAdmitted));
7782
7888
  }
7783
7889
  if (kinds.length > SCRIPT_EXPANSION_MAX_COMMANDS) return false;
@@ -7802,7 +7908,11 @@ var SCRIPT_ALIAS_TOOLS = /* @__PURE__ */ new Set(["pdm", "hatch", "pipenv"]);
7802
7908
  var PDM_RUN_HOOKS = ["pre_run", "post_run", "pre_script", "post_script", "_"];
7803
7909
  var HATCH_UNREAD_SETTING_PATTERN = /env-vars|pre-install-commands|post-install-commands/;
7804
7910
  var HATCH_ENV_ADMITTED_OPTIONS = /* @__PURE__ */ new Set(["skip-install", "description", "dependencies", "extra-dependencies"]);
7805
- var HATCH_INSTALLS_PROJECT_REASON = "the check runs hatch run in an environment that does not set skip-install, so hatch first installs the project, running its build code";
7911
+ var PROJECT_INSTALL_REASON = (runs, tool) => `the check runs ${runs}, so ${tool} first installs the project, running its build code`;
7912
+ var HATCH_INSTALLS_PROJECT_REASON = PROJECT_INSTALL_REASON("hatch run in an environment that does not set skip-install", "hatch");
7913
+ var UV_INSTALLS_PROJECT_REASON = PROJECT_INSTALL_REASON("uv run in a project uv builds", "uv");
7914
+ var UV_SYNCED_PROJECT_REASON = PROJECT_INSTALL_REASON("uv run --no-sync after a uv sync of a project uv builds", "uv");
7915
+ var UV_RUN_UNREAD_REASON = CONFIGURATION_UNREAD_REASON("uv run");
7806
7916
  function readHatchSkipInstall(fileSet, readText, isIndexName) {
7807
7917
  const skip = /* @__PURE__ */ new Map();
7808
7918
  for (const [file, prefix] of [["hatch.toml", []], ["pyproject.toml", ["tool", "hatch"]]]) {
@@ -7895,7 +8005,7 @@ function scriptAliasBodyKinds(text, shell, tscAdmitted) {
7895
8005
  if (shell ? !commandsAreAndChain(text) : /[|&;<>$`(){}\\\r\n]/.test(text)) return ["other"];
7896
8006
  const commands = shell ? shellCommands(text) : [shellWords(text)];
7897
8007
  return commands.map((words) => {
7898
- const peeled = peelCommandLaunchers(words);
8008
+ const peeled = peelCommandLaunchers(words, tscAdmitted);
7899
8009
  return peeled === null ? "other" : scriptCommandKind(peeled, tscAdmitted);
7900
8010
  });
7901
8011
  }
@@ -8048,13 +8158,200 @@ var CHECK_FORMS = new Map([
8048
8158
  ["pyright", [checkForm([], ["--warnings"], ["-p", "--project", "--pythonversion"])]],
8049
8159
  ...["ava", "uvu", "tap", "tape", "jasmine", "qunit", "lab", "ts-mocha"].map((runner) => [runner, [PLAIN_RUNNER_FORM]])
8050
8160
  ]);
8051
- function checkFormAdmits(words) {
8161
+ var POSITIONAL_READINGS = /* @__PURE__ */ new Map([
8162
+ ["pytest", "path"],
8163
+ ["mocha", "path"],
8164
+ ["node", "path"],
8165
+ ["tsx", "path"],
8166
+ ["deno", "path"],
8167
+ ["rspec", "path"],
8168
+ ["phpunit", "path"],
8169
+ ["dotnet", "path"],
8170
+ ["ava", "path"],
8171
+ ["tap", "path"],
8172
+ ["tape", "path"],
8173
+ ["jasmine", "path"],
8174
+ ["qunit", "path"],
8175
+ ["lab", "path"],
8176
+ ["ts-mocha", "path"],
8177
+ ["go", "go"],
8178
+ ["unittest", "module"],
8179
+ ["uvu", "uvu"]
8180
+ ]);
8181
+ var UNRESOLVED_PATH_WORD_PATTERN = /[$`'"{}\\[\]]|^~/;
8182
+ var GLOB_WORD_PATTERN = /[*?]/;
8183
+ var TEST_PATH_GLOB_BUDGET = 2e6;
8184
+ function repositoryRelativePath(word) {
8185
+ if (word.startsWith("/")) return null;
8186
+ const parts = [];
8187
+ for (const part of word.split("/")) {
8188
+ if (part === "" || part === ".") continue;
8189
+ if (part === "..") {
8190
+ if (parts.length === 0) return null;
8191
+ parts.pop();
8192
+ } else parts.push(part);
8193
+ }
8194
+ return parts.join("/");
8195
+ }
8196
+ function globMatchesPath(glob, path, budget) {
8197
+ const part = (pattern, text) => {
8198
+ budget.steps -= (pattern.length + 1) * (text.length + 1);
8199
+ if (budget.steps < 0) return void 0;
8200
+ let p = 0;
8201
+ let t = 0;
8202
+ let star = -1;
8203
+ let mark = 0;
8204
+ while (t < text.length) {
8205
+ if (p < pattern.length && (pattern[p] === "?" || pattern[p] === text[t])) {
8206
+ p += 1;
8207
+ t += 1;
8208
+ } else if (p < pattern.length && pattern[p] === "*") {
8209
+ star = p;
8210
+ mark = t;
8211
+ p += 1;
8212
+ } else if (star !== -1) {
8213
+ p = star + 1;
8214
+ mark += 1;
8215
+ t = mark;
8216
+ } else return false;
8217
+ }
8218
+ while (p < pattern.length && pattern[p] === "*") p += 1;
8219
+ return p === pattern.length;
8220
+ };
8221
+ let reachable = new Array(path.length + 1).fill(false);
8222
+ reachable[0] = true;
8223
+ for (const pattern of glob) {
8224
+ budget.steps -= path.length + 1;
8225
+ if (budget.steps < 0) return void 0;
8226
+ const next = new Array(path.length + 1).fill(false);
8227
+ for (let at = 0; at <= path.length; at += 1) {
8228
+ if (!reachable[at]) continue;
8229
+ if (pattern === "**") {
8230
+ for (let end = at; end <= path.length; end += 1) next[end] = true;
8231
+ break;
8232
+ }
8233
+ if (at === path.length) continue;
8234
+ const matched = part(pattern, path[at] ?? "");
8235
+ if (matched === void 0) return void 0;
8236
+ if (matched) next[at + 1] = true;
8237
+ }
8238
+ reachable = next;
8239
+ }
8240
+ return reachable[path.length] === true;
8241
+ }
8242
+ function testPathReader(testFiles) {
8243
+ let read;
8244
+ const budget = { steps: TEST_PATH_GLOB_BUDGET };
8245
+ const inventory = () => {
8246
+ if (read !== void 0) return read;
8247
+ const trie = { directories: /* @__PURE__ */ new Map(), files: /* @__PURE__ */ new Set(), goPackages: /* @__PURE__ */ new Set(), goTrees: /* @__PURE__ */ new Set(), parts: testFiles().map((file) => file.split("/")) };
8248
+ for (const file of trie.parts) {
8249
+ const goTest = (file[file.length - 1] ?? "").endsWith("_test.go");
8250
+ let skipped = false;
8251
+ let node = 0;
8252
+ if (goTest) trie.goTrees.add(node);
8253
+ for (let at = 0; at < file.length - 1; at += 1) {
8254
+ const name = file[at] ?? "";
8255
+ const key = `${node}/${name}`;
8256
+ let next = trie.directories.get(key);
8257
+ if (next === void 0) {
8258
+ next = trie.directories.size + 1;
8259
+ trie.directories.set(key, next);
8260
+ }
8261
+ node = next;
8262
+ skipped ||= name === "testdata" || name.startsWith(".") || name.startsWith("_");
8263
+ if (goTest && !skipped) trie.goTrees.add(node);
8264
+ }
8265
+ trie.files.add(`${node}/${file[file.length - 1] ?? ""}`);
8266
+ if (goTest && !skipped) trie.goPackages.add(node);
8267
+ }
8268
+ read = trie;
8269
+ return read;
8270
+ };
8271
+ const child = (node, name) => inventory().directories.get(`${node}/${name}`);
8272
+ const hasFile = (node, name) => inventory().files.has(`${node}/${name}`);
8273
+ const directoryAt = (parts) => {
8274
+ let node = 0;
8275
+ for (const name of parts) {
8276
+ node = child(node, name);
8277
+ if (node === void 0) return void 0;
8278
+ }
8279
+ return node;
8280
+ };
8281
+ const listed = (path) => {
8282
+ const parts = path === "" ? [] : path.split("/");
8283
+ if (parts.length === 0) return inventory().parts.length > 0;
8284
+ if (directoryAt(parts) !== void 0) return true;
8285
+ const parent = directoryAt(parts.slice(0, -1));
8286
+ return parent !== void 0 && hasFile(parent, parts[parts.length - 1] ?? "");
8287
+ };
8288
+ const pathReading = (word) => {
8289
+ if (GLOB_WORD_PATTERN.test(word)) {
8290
+ const glob = repositoryRelativePath(word);
8291
+ if (glob === null) return "no-test";
8292
+ const pattern = glob.split("/");
8293
+ for (const file of inventory().parts) {
8294
+ const matched = globMatchesPath(pattern, file, budget);
8295
+ if (matched === void 0) return "unread";
8296
+ if (matched) return "test";
8297
+ }
8298
+ return "no-test";
8299
+ }
8300
+ const path = repositoryRelativePath(word);
8301
+ return path !== null && listed(path) ? "test" : "no-test";
8302
+ };
8303
+ return (word, kind, fromRoot) => {
8304
+ if (!fromRoot || word === "" || UNRESOLVED_PATH_WORD_PATTERN.test(word)) return "unread";
8305
+ if (kind === "go") {
8306
+ if (GLOB_WORD_PATTERN.test(word)) return "unread";
8307
+ const tree = word === "..." || word.endsWith("/...");
8308
+ const directory = tree ? word.slice(0, -3) : word;
8309
+ if (!(directory === "" || directory === "." || directory.startsWith("./") || directory.startsWith("../") || directory === "..")) return "unread";
8310
+ if (directory.includes("...")) return "unread";
8311
+ const path = repositoryRelativePath(directory);
8312
+ if (path === null) return "no-test";
8313
+ const node = directoryAt(path === "" ? [] : path.split("/"));
8314
+ return node !== void 0 && (tree ? inventory().goTrees : inventory().goPackages).has(node) ? "test" : "no-test";
8315
+ }
8316
+ if (kind === "module" && !word.includes("/") && !word.endsWith(".py")) {
8317
+ const parts = word.split(".");
8318
+ if (parts.some((name) => name === "")) return "unread";
8319
+ let node = 0;
8320
+ for (const name of parts) {
8321
+ if (hasFile(node, `${name}.py`)) return "test";
8322
+ node = child(node, name);
8323
+ if (node === void 0) return "no-test";
8324
+ }
8325
+ return "test";
8326
+ }
8327
+ return pathReading(word);
8328
+ };
8329
+ }
8330
+ var NAMEABLE_POSITIONAL_WORD_PATTERN = /^[A-Za-z0-9_./:*?@+=,-]{1,80}$/;
8331
+ var POSITIONAL_NO_TEST_REASON = (word) => NAMEABLE_POSITIONAL_WORD_PATTERN.test(word) ? `the check points at \`${word}\`, where Cejel finds no test file` : "the check points at a path where Cejel finds no test file";
8332
+ function positionalProblem(runner, positionals, testPath) {
8333
+ const kind = POSITIONAL_READINGS.get(runner);
8334
+ if (kind === void 0) return void 0;
8335
+ for (const [at, positional] of positionals.entries()) {
8336
+ if (kind === "module" && at === 0 && positional === "discover") {
8337
+ if (positionals.length > 1) return { word: positional, unread: true };
8338
+ continue;
8339
+ }
8340
+ if (kind === "uvu" && at > 0) return { word: positional, unread: true };
8341
+ const word = runner === "pytest" && positional.includes("::") ? positional.slice(0, positional.indexOf("::")) : positional;
8342
+ const reading = testPath(word, kind === "uvu" ? "path" : kind);
8343
+ if (reading !== "test") return { word: positional, unread: reading === "unread" };
8344
+ }
8345
+ return void 0;
8346
+ }
8347
+ function checkFormAdmits(words, testPath) {
8052
8348
  const forms = CHECK_FORMS.get(words[0] ?? "");
8053
8349
  if (forms === void 0) return void 0;
8054
8350
  const form = forms.find(({ sub }) => sub.every((word, at) => words[1 + at] === word));
8055
8351
  if (form === void 0) return false;
8056
8352
  const args = words.slice(1 + form.sub.length);
8057
8353
  let positionals = 0;
8354
+ const paths = [];
8058
8355
  for (let index = 0; index < args.length; index += 1) {
8059
8356
  const arg = args[index] ?? "";
8060
8357
  if (arg === "--") return form.afterSeparator !== void 0 && args.slice(index + 1).every((word) => form.afterSeparator?.has(word));
@@ -8078,17 +8375,22 @@ function checkFormAdmits(words) {
8078
8375
  if (form.firstPositional !== void 0 && positionals === 0) {
8079
8376
  if (!form.firstPositional.has(arg)) return false;
8080
8377
  } else if (!form.positional) return false;
8378
+ else paths.push(arg);
8081
8379
  positionals += 1;
8082
8380
  }
8083
- return true;
8381
+ if (testPath === void 0 || paths.length === 0) return true;
8382
+ return positionalProblem(words[0] ?? "", paths, testPath) ?? true;
8084
8383
  }
8085
8384
  var RUNNER_ARGUMENT_REASON = "the check carries a flag or argument that Cejel does not know to keep it failing when a test fails";
8385
+ function positionalReason(problem) {
8386
+ return problem.unread ? UNREAD_TEST_ARGUMENT_REASON : POSITIONAL_NO_TEST_REASON(problem.word);
8387
+ }
8086
8388
  function rootTestScriptLastRunner(root, inventory, tscAdmitted) {
8087
8389
  const scripts = rootPackageScripts(root, inventory);
8088
8390
  const body = scripts?.test;
8089
8391
  if (scripts === void 0 || typeof body !== "string" || !commandsAreAndChain(body)) return void 0;
8090
8392
  const commands = shellCommands(body);
8091
- const peeled = peelCommandLaunchers(commands[commands.length - 1] ?? []);
8393
+ const peeled = peelCommandLaunchers(commands[commands.length - 1] ?? [], tscAdmitted);
8092
8394
  if (peeled === null || packageScriptRun(peeled, scripts) !== void 0) return void 0;
8093
8395
  return scriptCommandKind(peeled, tscAdmitted) === "runner" ? peeled : void 0;
8094
8396
  }
@@ -9974,6 +10276,7 @@ var PYTEST_CONFIG_FILE_NAMES = ["pytest.toml", ".pytest.toml", "pytest.ini", ".p
9974
10276
  var PYTEST_EXECUTABLE_PATTERN = /^(?:pytest|py\.test)(?:\.exe)?$/;
9975
10277
  var PYTEST_ADDOPTS_ASSIGNMENT = "PYTEST_ADDOPTS=";
9976
10278
  var PYTEST_ADDOPTS_LINE_PATTERN = /^[ \t]*(?:-[ \t]*)?(?:export[ \t]+)?PYTEST_ADDOPTS[ \t]*(?::|[:?+]?=)(.*)$/;
10279
+ var PYTEST_ADDOPTS_KEY_PATTERN = /^[ \t]*PYTEST_ADDOPTS(?:[ \t]*:|[ \t]+[:?+]?=)/;
9977
10280
  var CI_SCRIPT_KEY_PATTERN = /^([ \t]*)(?:-[ \t]*)?(?:script|before_script|after_script|bash|pwsh|powershell|sh|command)[ \t]*:[ \t]*(.*)$/;
9978
10281
  var A1_NOTE_PATH_MAX_LENGTH = 120;
9979
10282
  function a1NotePath(path) {
@@ -10522,10 +10825,11 @@ function resolvePytestRun(invocation, repo) {
10522
10825
  const commandAt = (tokens) => (token, label) => () => file === "" ? void 0 : { path: file, line: repo.lineOf(file, [token, ...tokens], anchor), label };
10523
10826
  const groups = [];
10524
10827
  if (invocation.addopts && invocation.addopts.length > 0) {
10828
+ const elsewhere = invocation.addoptsAt;
10525
10829
  groups.push({
10526
10830
  words: invocation.addopts,
10527
- where: `in PYTEST_ADDOPTS in ${a1NotePath(invocation.source ?? "")}`,
10528
- at: (_token, label) => () => file === "" ? void 0 : { path: file, line: repo.lineOf(file, ["PYTEST_ADDOPTS"], invocation.addoptsAnchor ?? anchor), label }
10831
+ where: `in PYTEST_ADDOPTS in ${a1NotePath(elsewhere?.source ?? invocation.source ?? "")}`,
10832
+ at: (_token, label) => () => elsewhere !== void 0 ? { path: elsewhere.file, line: repo.lineOf(elsewhere.file, ["PYTEST_ADDOPTS"], elsewhere.line), label } : file === "" ? void 0 : { path: file, line: repo.lineOf(file, ["PYTEST_ADDOPTS"], invocation.addoptsAnchor ?? anchor), label }
10529
10833
  });
10530
10834
  }
10531
10835
  if (invocation.args.length > 0) groups.push({ words: invocation.args, where: commandWhere, at: commandAt(["pytest", "py.test"]) });
@@ -11036,7 +11340,7 @@ function stripShellHeredocBodies(text) {
11036
11340
  }
11037
11341
  return kept.join("\n");
11038
11342
  }
11039
- function readShellCommands(text, directory, addopts, visit, unknownDirectory) {
11343
+ function readShellCommands(text, directory, addopts, visit, unknownDirectory, mentions) {
11040
11344
  let current = directory;
11041
11345
  let why = directory === null ? unknownDirectory : void 0;
11042
11346
  let exported = addopts;
@@ -11048,9 +11352,13 @@ function readShellCommands(text, directory, addopts, visit, unknownDirectory) {
11048
11352
  if (start >= all.length) continue;
11049
11353
  all[start] = (all[start] ?? "").replace(/^[({]+/, "").replace(/^[@+-]+(?=[A-Za-z./{$])/, "");
11050
11354
  let local = exported;
11355
+ let leading = 0;
11051
11356
  while (start < all.length && /^[A-Za-z_]\w*=/.test(all[start] ?? "")) {
11052
11357
  const assignment = all[start] ?? "";
11053
- if (assignment.startsWith(PYTEST_ADDOPTS_ASSIGNMENT)) local = shellWords(assignment.slice(PYTEST_ADDOPTS_ASSIGNMENT.length));
11358
+ if (assignment.startsWith(PYTEST_ADDOPTS_ASSIGNMENT)) {
11359
+ local = shellWords(assignment.slice(PYTEST_ADDOPTS_ASSIGNMENT.length));
11360
+ leading += 1;
11361
+ }
11054
11362
  start += 1;
11055
11363
  }
11056
11364
  const words = start === 0 ? all : all.slice(start);
@@ -11061,10 +11369,17 @@ function readShellCommands(text, directory, addopts, visit, unknownDirectory) {
11061
11369
  const name = words[0] ?? "";
11062
11370
  if (name === "export") {
11063
11371
  for (const word of words.slice(1)) {
11064
- if (word.startsWith(PYTEST_ADDOPTS_ASSIGNMENT)) exported = shellWords(word.slice(PYTEST_ADDOPTS_ASSIGNMENT.length));
11372
+ if (word.startsWith(PYTEST_ADDOPTS_ASSIGNMENT)) {
11373
+ exported = shellWords(word.slice(PYTEST_ADDOPTS_ASSIGNMENT.length));
11374
+ if (mentions !== void 0) mentions.read += 1;
11375
+ }
11065
11376
  }
11066
11377
  continue;
11067
11378
  }
11379
+ if (mentions !== void 0 && (leading > 0 || name === "env")) {
11380
+ const make = makeCommandOf(words);
11381
+ if (make !== void 0) mentions.read += leading + make.addoptsAssignments;
11382
+ }
11068
11383
  if (name === "cd" || name === "pushd") {
11069
11384
  const target = words[1];
11070
11385
  const before = current;
@@ -11370,6 +11685,13 @@ function readGithubRunSteps(text) {
11370
11685
  return /* @__PURE__ */ new Map();
11371
11686
  };
11372
11687
  const scalarAt = (index) => index === void 0 ? void 0 : yamlPlainScalar(lines[index]?.value ?? "");
11688
+ const writtenAt = (index) => {
11689
+ const value = lines[index]?.value ?? "";
11690
+ const scalar = yamlPlainScalar(value);
11691
+ const quote = value[0];
11692
+ const close = quote === '"' || quote === "'" ? value.lastIndexOf(quote) : -1;
11693
+ return close > 0 && value.slice(1, close) === scalar ? value.slice(0, close + 1) : scalar;
11694
+ };
11373
11695
  const isFalse = (value) => value !== void 0 && /^(?:false|\$\{\{\s*false\s*\}\})$/.test(value);
11374
11696
  const defaultDirectory = (keys) => scalarAt(childKeys(childKeys(keys.get("defaults")).get("run")).get("working-directory"));
11375
11697
  const addoptsLineIn = (keys) => childKeys(keys.get("env")).get("PYTEST_ADDOPTS");
@@ -11429,13 +11751,17 @@ function readGithubRunSteps(text) {
11429
11751
  } else {
11430
11752
  command = yamlPlainScalar(line.value);
11431
11753
  }
11754
+ const expression = addoptsValue !== void 0 && addoptsValue.includes("${{");
11755
+ const stepAddoptsLine = addoptsLineIn(stepKeys);
11432
11756
  steps.push({
11433
11757
  command,
11434
11758
  directory,
11435
11759
  line: index,
11760
+ addoptsKeyLines: [root.addoptsLine, job.addoptsLine, stepAddoptsLine].filter((at) => at !== void 0),
11436
11761
  ...directory === null && directoryValue !== void 0 ? { unknownDirectory: `working-directory: ${directoryValue}` } : {},
11437
- ...addoptsValue === void 0 || addoptsValue.includes("${{") ? {} : { addopts: shellWords(addoptsValue) },
11438
- ...addoptsValue === void 0 || addoptsValue.includes("${{") || addoptsLine === void 0 ? {} : { addoptsLine }
11762
+ ...addoptsValue === void 0 || expression ? {} : { addopts: shellWords(addoptsValue) },
11763
+ ...addoptsValue === void 0 || addoptsLine === void 0 ? {} : { addoptsLine },
11764
+ ...expression ? { addoptsNotKnown: `PYTEST_ADDOPTS: ${addoptsLine === void 0 ? addoptsValue : writtenAt(addoptsLine)}` } : {}
11439
11765
  });
11440
11766
  }
11441
11767
  return steps;
@@ -11452,8 +11778,30 @@ function readPytestInvocations(repoFiles, readText) {
11452
11778
  let hatchScripts;
11453
11779
  let hatchExpansionsLeft = HATCH_EXPANSION_BUDGET;
11454
11780
  const environmentTexts = [];
11455
- const visitor = (source, file, anchor, depth = 0, envAddopts) => (words, directory, addopts, unknownDirectory) => {
11781
+ let addoptsMentionNotRead = false;
11782
+ const noteAddoptsMentions = (text, read) => {
11783
+ if (wordCount(text, "PYTEST_ADDOPTS") !== read) addoptsMentionNotRead = true;
11784
+ };
11785
+ const makeEnvironments = [];
11786
+ let readingCi = true;
11787
+ const visitor = (source, file, anchor, depth = 0, envAddopts, envNotKnown, addoptsAt) => (words, directory, addopts, unknownDirectory) => {
11456
11788
  for (const target of recipeTargetsNamed(words)) namedTargets.add(target);
11789
+ const makeRun = readingCi ? makeCommandOf(words) : void 0;
11790
+ if (makeRun !== void 0) {
11791
+ const inForce2 = makeRun.addopts ?? addopts;
11792
+ const notKnown = envNotKnown?.reason ?? (inForce2?.some((word) => /[$`]/.test(word)) ? `PYTEST_ADDOPTS=${inForce2.join(" ")}` : void 0);
11793
+ if (inForce2 !== void 0 || notKnown !== void 0) {
11794
+ let runIn2 = makeRun.namesFile ? null : directory;
11795
+ for (const step of makeRun.directories) runIn2 = runIn2 === null ? null : resolveCommandPath(runIn2, step);
11796
+ makeEnvironments.push({
11797
+ directory: runIn2,
11798
+ targets: makeRun.targets,
11799
+ overrides: makeRun.overrides,
11800
+ ...notKnown !== void 0 ? { notKnown } : inForce2 !== void 0 ? { addopts: inForce2 } : {},
11801
+ at: { source, file, line: envAddopts !== void 0 && inForce2 === envAddopts.words ? envAddopts.line : anchor }
11802
+ });
11803
+ }
11804
+ }
11457
11805
  for (const name of makeAssignmentsNamed(words)) makeOverrides.add(name);
11458
11806
  for (const environment of toxEnvironmentsNamed(words)) namedToxEnvironments.add(environment);
11459
11807
  const posargs = toxPosargsOf(words);
@@ -11488,7 +11836,7 @@ function readPytestInvocations(repoFiles, readText) {
11488
11836
  const substituted = substituteHatchArguments(command, hatch.args);
11489
11837
  const first = substituted.trim().split(/\s+/)[0] ?? "";
11490
11838
  const inner = hatchScripts.get(hatch.environment)?.has(first) ? `hatch run ${hatch.environment}:${substituted.trim()}` : substituted;
11491
- readShellCommands(inner, directory === null ? null : repoDirectory(script.file), addopts, visitor(a1NotePath(script.file), script.file, script.line, depth + 1), unknownDirectory);
11839
+ readShellCommands(inner, directory === null ? null : repoDirectory(script.file), addopts, visitor(a1NotePath(script.file), script.file, script.line, depth + 1, void 0, envNotKnown), unknownDirectory);
11492
11840
  }
11493
11841
  return;
11494
11842
  }
@@ -11508,7 +11856,12 @@ function readPytestInvocations(repoFiles, readText) {
11508
11856
  return;
11509
11857
  }
11510
11858
  const inForce = found.addopts ?? addopts;
11859
+ if (inForce === void 0 && envNotKnown !== void 0) {
11860
+ invocations.push({ source: envNotKnown.source, file: envNotKnown.file, anchor: envNotKnown.line, directory: "", args: [], text: textOf(), unread: envNotKnown.reason });
11861
+ return;
11862
+ }
11511
11863
  const addoptsAnchor = inForce !== void 0 && envAddopts !== void 0 && inForce === envAddopts.words ? envAddopts.line : void 0;
11864
+ const elsewhere = inForce !== void 0 && addoptsAt !== void 0 && inForce === addoptsAt.words ? addoptsAt : void 0;
11512
11865
  invocations.push({
11513
11866
  source,
11514
11867
  file,
@@ -11517,7 +11870,8 @@ function readPytestInvocations(repoFiles, readText) {
11517
11870
  directory: runIn,
11518
11871
  args: found.args,
11519
11872
  ...inForce === void 0 ? {} : { addopts: inForce },
11520
- ...addoptsAnchor === void 0 ? {} : { addoptsAnchor }
11873
+ ...addoptsAnchor === void 0 ? {} : { addoptsAnchor },
11874
+ ...elsewhere === void 0 ? {} : { addoptsAt: { source: elsewhere.source, file: elsewhere.file, line: elsewhere.line } }
11521
11875
  });
11522
11876
  };
11523
11877
  const usable = repoFiles.filter((file) => !GENERATED_OR_VENDOR_PATH_PATTERN.test(file));
@@ -11528,10 +11882,24 @@ function readPytestInvocations(repoFiles, readText) {
11528
11882
  const remote = readGithubRemoteSteps(text);
11529
11883
  const writesAddopts = text.includes("GITHUB_ENV") && text.includes("PYTEST_ADDOPTS") ? "PYTEST_ADDOPTS written to $GITHUB_ENV" : void 0;
11530
11884
  const unreadWhy = writesAddopts ?? remote.checkoutPath;
11885
+ const keyLines = /* @__PURE__ */ new Set();
11886
+ const shellMentions = writesAddopts === void 0 && text.includes("PYTEST_ADDOPTS") ? { read: 0 } : void 0;
11531
11887
  for (const step of readGithubRunSteps(text)) {
11888
+ for (const at of step.addoptsKeyLines) keyLines.add(at);
11532
11889
  const envAddopts = step.addopts !== void 0 && step.addoptsLine !== void 0 ? { words: step.addopts, line: step.addoptsLine } : void 0;
11533
- if (unreadWhy !== void 0) readShellCommands(step.command, null, step.addopts, visitor(source, file, step.line, 0, envAddopts), unreadWhy);
11534
- else readShellCommands(step.command, step.directory, step.addopts, visitor(source, file, step.line, 0, envAddopts), step.unknownDirectory);
11890
+ const notKnownReason = writesAddopts ?? step.addoptsNotKnown;
11891
+ const envNotKnown = notKnownReason === void 0 ? void 0 : { reason: notKnownReason, source, file, line: writesAddopts === void 0 && step.addoptsLine !== void 0 ? step.addoptsLine : step.line };
11892
+ if (unreadWhy !== void 0) readShellCommands(step.command, null, step.addopts, visitor(source, file, step.line, 0, envAddopts, envNotKnown), unreadWhy, shellMentions);
11893
+ else readShellCommands(step.command, step.directory, step.addopts, visitor(source, file, step.line, 0, envAddopts, envNotKnown), step.unknownDirectory, shellMentions);
11894
+ }
11895
+ if (text.includes("PYTEST_ADDOPTS")) {
11896
+ let read = keyLines.size + (shellMentions?.read ?? 0);
11897
+ if (writesAddopts !== void 0) {
11898
+ text.split(/\r?\n/).forEach((line, index) => {
11899
+ if (!keyLines.has(index) && line.includes("GITHUB_ENV")) read += wordCount(line, "PYTEST_ADDOPTS");
11900
+ });
11901
+ }
11902
+ noteAddoptsMentions(text, read);
11535
11903
  }
11536
11904
  for (const step of remote.pytestInputs) {
11537
11905
  invocations.push({ source, file, anchor: step.line, directory: "", args: [], text: step.uses, unread: step.uses });
@@ -11566,8 +11934,13 @@ function readPytestInvocations(repoFiles, readText) {
11566
11934
  environmentTexts.push(text);
11567
11935
  const jenkinsDir = file === "Jenkinsfile" ? jenkinsDirCall(text) : void 0;
11568
11936
  const addopts = readCiAddopts(text);
11937
+ const shellMentions = text.includes("PYTEST_ADDOPTS") ? { read: 0 } : void 0;
11569
11938
  for (const command of ciShellTexts(text)) {
11570
- readShellCommands(command, jenkinsDir === void 0 ? "" : null, addopts, visitor(source, file, 0), jenkinsDir);
11939
+ readShellCommands(command, jenkinsDir === void 0 ? "" : null, addopts, visitor(source, file, 0), jenkinsDir, shellMentions);
11940
+ }
11941
+ 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));
11571
11944
  }
11572
11945
  }
11573
11946
  const noxAt = invocations.length;
@@ -11622,18 +11995,22 @@ function readPytestInvocations(repoFiles, readText) {
11622
11995
  }
11623
11996
  return noxRuns;
11624
11997
  };
11998
+ readingCi = false;
11999
+ const environments = distinctMakeEnvironments(makeEnvironments);
11625
12000
  for (const file of usable.filter(isRecipeFile)) {
11626
12001
  const text = readText(file);
11627
12002
  if (text === void 0) continue;
11628
12003
  const just = /[Jj]ustfile$/.test(file);
11629
12004
  const targets = just ? readJustRecipes(text) : readRecipeTargets(text);
11630
12005
  const headers = just ? new Map([...readJustRecipes(text)].map(([name, recipe]) => [name, recipe.line])) : makeTargetLines(text);
11631
- const setByEnvironment = (name) => environmentTexts.some((ciText) => containsWord(ciText, name));
12006
+ const setByEnvironment = (name) => name === "PYTEST_ADDOPTS" ? addoptsMentionNotRead : environmentTexts.some((ciText) => containsWord(ciText, name));
11632
12007
  const justVariables = just ? readJustVariables(text) : void 0;
11633
12008
  const make = just ? void 0 : readMakeVariables(text, setByEnvironment);
11634
12009
  const budget = { left: MAKE_EXPANSION_BUDGET };
11635
- const isUnknown = (name) => make?.unknown.has(name) === true || makeOverrides.has(name);
11636
- const expand = (command) => justVariables !== void 0 ? { text: expandJustVariables(command, justVariables), unknown: false } : expandMakeVariables(command, make?.values ?? /* @__PURE__ */ new Map(), isUnknown, budget);
12010
+ const unknownIn = (variables) => (name) => variables?.unknown.has(name) === true || makeOverrides.has(name);
12011
+ const isUnknown = unknownIn(make);
12012
+ const expandWith = (variables) => (command) => justVariables !== void 0 ? { text: expandJustVariables(command, justVariables), unknown: false } : expandMakeVariables(command, variables?.values ?? /* @__PURE__ */ new Map(), unknownIn(variables), budget);
12013
+ const expand = expandWith(make);
11637
12014
  let addoptsUnknown = false;
11638
12015
  let addoptsText;
11639
12016
  if (justVariables !== void 0) {
@@ -11655,30 +12032,63 @@ function readPytestInvocations(repoFiles, readText) {
11655
12032
  }
11656
12033
  const addopts = addoptsText === void 0 ? void 0 : shellWords(shellWords(addoptsText.trim()).join(" "));
11657
12034
  const unknownRunsPytest = (command, expanded) => command.toLowerCase().includes("pytest") || expanded.toLowerCase().includes("pytest") || makeReferencesIn(expanded).some((name) => make?.pytestNames.has(name) === true);
11658
- const reached = /* @__PURE__ */ new Set();
11659
- const queue = [[...targets.keys()][0], "test", "tests", "check", ...namedTargets].filter(
11660
- (name) => name !== void 0 && targets.has(name)
11661
- );
11662
- while (queue.length > 0) {
11663
- const name = queue.shift() ?? "";
11664
- if (reached.has(name)) continue;
11665
- reached.add(name);
11666
- const target = targets.get(name);
11667
- if (!target) continue;
11668
- queue.push(...target.dependencies.filter((dependency) => targets.has(dependency)));
11669
- const commands = joinRecipeContinuations(target.commands);
11670
- for (const command of commands) {
11671
- const expanded = expand(command);
11672
- if (expanded.unknown && unknownRunsPytest(command, expanded.text)) {
11673
- invocations.push({ source: a1NotePath(file), file, anchor: headers.get(name) ?? 0, directory: "", args: [], text: command, unread: command });
11674
- continue;
12035
+ const firstTarget = [...targets.keys()][0];
12036
+ const readRecipes = (names, variables, inForceAddopts, notKnown, addoptsAt) => {
12037
+ const expandHere = variables === make ? expand : expandWith(variables);
12038
+ const reached = /* @__PURE__ */ new Set();
12039
+ const queue = names.filter((name) => name !== void 0 && targets.has(name));
12040
+ while (queue.length > 0) {
12041
+ const name = queue.shift() ?? "";
12042
+ if (reached.has(name)) continue;
12043
+ reached.add(name);
12044
+ const target = targets.get(name);
12045
+ if (!target) continue;
12046
+ queue.push(...target.dependencies.filter((dependency) => targets.has(dependency)));
12047
+ const commands = joinRecipeContinuations(target.commands);
12048
+ for (const command of commands) {
12049
+ const expanded = expandHere(command);
12050
+ if (expanded.unknown && unknownRunsPytest(command, expanded.text)) {
12051
+ invocations.push({ source: a1NotePath(file), file, anchor: headers.get(name) ?? 0, directory: "", args: [], text: command, unread: command });
12052
+ continue;
12053
+ }
12054
+ readShellCommands(expanded.text, repoDirectory(file), inForceAddopts, (words, directory, inForce, why) => {
12055
+ 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);
12057
+ if (notKnown !== void 0) visit(words, null, inForce, notKnown);
12058
+ else visit(words, directory, inForce, why);
12059
+ });
11675
12060
  }
11676
- readShellCommands(expanded.text, repoDirectory(file), addopts, (words, directory, inForce, why) => {
11677
- queue.push(...recipeTargetsNamed(words).filter((named) => targets.has(named)));
11678
- if (addoptsUnknown) visitor(a1NotePath(file), file, headers.get(name) ?? 0)(words, null, inForce, "PYTEST_ADDOPTS");
11679
- else visitor(a1NotePath(file), file, headers.get(name) ?? 0)(words, directory, inForce, why);
11680
- });
11681
12061
  }
12062
+ };
12063
+ readRecipes([firstTarget, "test", "tests", "check", ...namedTargets], make, addopts, addoptsUnknown ? "PYTEST_ADDOPTS" : void 0);
12064
+ if (make === void 0 || addoptsUnknown) continue;
12065
+ const here = repoDirectory(file);
12066
+ const baseKey = addopts?.join("\0");
12067
+ for (const environment of environments) {
12068
+ if (environment.directory !== null && environment.directory !== here) continue;
12069
+ const names = environment.targets.map((target) => target === "" ? firstTarget : target);
12070
+ if (environment.notKnown !== void 0) {
12071
+ readRecipes(names, make, void 0, environment.notKnown);
12072
+ continue;
12073
+ }
12074
+ const words = environment.addopts ?? [];
12075
+ const local = readMakeVariables(text, setByEnvironment, { name: "PYTEST_ADDOPTS", value: words.join(" "), overrides: environment.overrides });
12076
+ if (local.unknown.has("PYTEST_ADDOPTS")) {
12077
+ readRecipes(names, local, void 0, "PYTEST_ADDOPTS");
12078
+ continue;
12079
+ }
12080
+ if (local.fromEnvironment) {
12081
+ readRecipes(names, local, words, void 0, { words, ...environment.at });
12082
+ continue;
12083
+ }
12084
+ const expanded = expandWith(local)(local.values.get("PYTEST_ADDOPTS") ?? "");
12085
+ if (expanded.unknown) {
12086
+ readRecipes(names, local, void 0, "PYTEST_ADDOPTS");
12087
+ continue;
12088
+ }
12089
+ const inForce = shellWords(shellWords(expanded.text.trim()).join(" "));
12090
+ if (inForce.join("\0") === baseKey) continue;
12091
+ readRecipes(names, local, inForce, void 0);
11682
12092
  }
11683
12093
  }
11684
12094
  const toxenv = readToxenvValues(environmentTexts);
@@ -11822,6 +12232,95 @@ function makeAssignmentsNamed(words) {
11822
12232
  return equals > 0 && /^[A-Za-z_][\w.-]*$/.test(word.slice(0, equals)) ? [word.slice(0, equals)] : [];
11823
12233
  });
11824
12234
  }
12235
+ var MAKE_LONG_OPTIONS_WITH_VALUE = /* @__PURE__ */ new Set([
12236
+ "--directory",
12237
+ "--file",
12238
+ "--makefile",
12239
+ "--include-dir",
12240
+ "--old-file",
12241
+ "--assume-old",
12242
+ "--new-file",
12243
+ "--assume-new",
12244
+ "--what-if",
12245
+ "--jobs",
12246
+ "--load-average",
12247
+ "--max-load",
12248
+ "--output-sync",
12249
+ "--debug"
12250
+ ]);
12251
+ function makeCommandOf(words) {
12252
+ let index = 0;
12253
+ let addopts;
12254
+ let addoptsAssignments = 0;
12255
+ if (words[0] === "env") {
12256
+ index = 1;
12257
+ while ((words[index] ?? "").startsWith("-") || /^[A-Za-z_]\w*=/.test(words[index] ?? "")) {
12258
+ const assignment = words[index] ?? "";
12259
+ if (assignment.startsWith(PYTEST_ADDOPTS_ASSIGNMENT)) {
12260
+ addopts = shellWords(assignment.slice(PYTEST_ADDOPTS_ASSIGNMENT.length));
12261
+ addoptsAssignments += 1;
12262
+ }
12263
+ index += 1;
12264
+ }
12265
+ }
12266
+ const head = words[index] ?? "";
12267
+ const name = basename(head);
12268
+ if (name !== "make" && name !== "gmake" && !/^\$[({]MAKE[)}]$/.test(head)) return void 0;
12269
+ const directories = [];
12270
+ const targets = [];
12271
+ let overrides = false;
12272
+ let namesFile = false;
12273
+ for (index += 1; index < words.length; index += 1) {
12274
+ const word = words[index] ?? "";
12275
+ if (word.startsWith("--")) {
12276
+ const equals = word.indexOf("=");
12277
+ const option = equals === -1 ? word : word.slice(0, equals);
12278
+ const value = equals === -1 ? MAKE_LONG_OPTIONS_WITH_VALUE.has(option) ? words[index += 1] : void 0 : word.slice(equals + 1);
12279
+ if (option === "--environment-overrides") overrides = true;
12280
+ else if (option === "--directory" && value !== void 0) directories.push(value);
12281
+ else if (option === "--file" || option === "--makefile") namesFile = true;
12282
+ continue;
12283
+ }
12284
+ if (word.startsWith("-") && word.length > 1) {
12285
+ for (let at = 1; at < word.length; at += 1) {
12286
+ const flag = word[at] ?? "";
12287
+ if (flag === "e") overrides = true;
12288
+ else if ("CfIoW".includes(flag)) {
12289
+ const value = at + 1 < word.length ? word.slice(at + 1) : words[index += 1];
12290
+ if (flag === "C" && value !== void 0) directories.push(value);
12291
+ if (flag === "f") namesFile = true;
12292
+ break;
12293
+ } else if (flag === "j" || flag === "l") {
12294
+ if (at + 1 === word.length && /^\d+(?:\.\d+)?$/.test(words[index + 1] ?? "")) index += 1;
12295
+ break;
12296
+ }
12297
+ }
12298
+ continue;
12299
+ }
12300
+ if (!word.includes("=")) targets.push(word);
12301
+ }
12302
+ return { directories, targets, overrides, namesFile, addoptsAssignments, ...addopts === void 0 ? {} : { addopts } };
12303
+ }
12304
+ var MAKE_ENVIRONMENTS_READ = 16;
12305
+ function distinctMakeEnvironments(environments) {
12306
+ const byKey = /* @__PURE__ */ new Map();
12307
+ let rest;
12308
+ for (const environment of environments) {
12309
+ const key = [environment.directory ?? "", environment.notKnown ?? "", ...environment.addopts ?? [], String(environment.overrides)].join("\0");
12310
+ let entry = byKey.get(key);
12311
+ if (entry === void 0 && byKey.size < MAKE_ENVIRONMENTS_READ) {
12312
+ entry = { environment, targets: /* @__PURE__ */ new Set() };
12313
+ byKey.set(key, entry);
12314
+ }
12315
+ if (entry === void 0) {
12316
+ rest ??= { environment: { directory: null, targets: [], overrides: false, notKnown: "PYTEST_ADDOPTS", at: environment.at }, targets: /* @__PURE__ */ new Set() };
12317
+ entry = rest;
12318
+ }
12319
+ if (environment.targets.length === 0) entry.targets.add("");
12320
+ for (const target of environment.targets) entry.targets.add(target);
12321
+ }
12322
+ return [...byKey.values(), ...rest === void 0 ? [] : [rest]].map(({ environment, targets }) => ({ ...environment, targets: [...targets] }));
12323
+ }
11825
12324
  function asToxCommand(words) {
11826
12325
  return asPythonToolCommand(words, "tox");
11827
12326
  }
@@ -11860,12 +12359,14 @@ function makeTargetLines(text) {
11860
12359
  return lines;
11861
12360
  }
11862
12361
  var MAKE_EXPANSION_BUDGET = 4096;
11863
- function readMakeVariables(text, setByEnvironment = () => false) {
12362
+ function readMakeVariables(text, setByEnvironment = () => false, environment) {
11864
12363
  const values = /* @__PURE__ */ new Map();
11865
12364
  const simple = /* @__PURE__ */ new Set();
11866
12365
  const unknown = /* @__PURE__ */ new Set();
11867
12366
  const pytestNames = /* @__PURE__ */ new Set();
11868
12367
  const budget = { left: MAKE_EXPANSION_BUDGET };
12368
+ let fromEnvironment = environment !== void 0;
12369
+ if (environment !== void 0) values.set(environment.name, environment.value);
11869
12370
  const forget = (name) => {
11870
12371
  unknown.add(name);
11871
12372
  values.delete(name);
@@ -11908,10 +12409,13 @@ function readMakeVariables(text, setByEnvironment = () => false) {
11908
12409
  const value = (match[3] ?? "").trim();
11909
12410
  if (value.toLowerCase().includes("pytest")) pytestNames.add(name);
11910
12411
  if (unknown.has(name)) continue;
12412
+ const fromTheEnvironment = environment !== void 0 && name === environment.name;
12413
+ if (fromTheEnvironment && environment.overrides && !/^override[ \t]/.test(line)) continue;
11911
12414
  if (conditional > 0 || operator === "!=" || operator === ":::=" || operator === "?=" && setByEnvironment(name)) {
11912
12415
  forget(name);
11913
12416
  continue;
11914
12417
  }
12418
+ if (fromTheEnvironment && operator !== "?=") fromEnvironment = false;
11915
12419
  if (operator === "?=") {
11916
12420
  if (!values.has(name)) {
11917
12421
  values.set(name, value);
@@ -11935,7 +12439,7 @@ function readMakeVariables(text, setByEnvironment = () => false) {
11935
12439
  simple.delete(name);
11936
12440
  }
11937
12441
  }
11938
- return { values, unknown, pytestNames };
12442
+ return { values, unknown, pytestNames, fromEnvironment };
11939
12443
  }
11940
12444
  function expandMakeVariables(command, values, isUnknown, budget, depth = 0) {
11941
12445
  if (!command.includes("$")) return { text: command, unknown: false };
@@ -11986,6 +12490,15 @@ function containsWord(text, name) {
11986
12490
  }
11987
12491
  return false;
11988
12492
  }
12493
+ function wordCount(text, name) {
12494
+ if (name === "") return 0;
12495
+ const word = (code) => code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122 || code === 95;
12496
+ let count = 0;
12497
+ for (let at = text.indexOf(name); at !== -1; at = text.indexOf(name, at + 1)) {
12498
+ if ((at === 0 || !word(text.charCodeAt(at - 1))) && (at + name.length >= text.length || !word(text.charCodeAt(at + name.length)))) count += 1;
12499
+ }
12500
+ return count;
12501
+ }
11989
12502
  function makeReferencesIn(text) {
11990
12503
  const names = [];
11991
12504
  for (let at = text.indexOf("$"); at !== -1; at = text.indexOf("$", at + 1)) {
@@ -12372,6 +12885,7 @@ function isGovernedPytestFile(skip, file) {
12372
12885
  var PYTEST_DEFAULT_NORECURSE_SEGMENT_PATTERN = /^(?:.*\.egg|\..*|_darcs|build|CVS|dist|node_modules|venv|\{arch\})$/;
12373
12886
  var PYTEST_COLLECT_IGNORE_PATTERN = /\b(?:collect_ignore|collect_ignore_glob|pytest_ignore_collect)\b/;
12374
12887
  var PYTEST_COLLECTION_HOOK_PATTERN = /^def[ \t]+pytest_collection_modifyitems[ \t]*\(/;
12888
+ var PYTEST_CLASS_COLLECTION_HOOK_PATTERN = /^[ \t]+(?:async[ \t]+)?def[ \t]+pytest_collection_modifyitems[ \t]*\(/;
12375
12889
  var PYTEST_COLLECTION_HOOK_NAME = "pytest_collection_modifyitems";
12376
12890
  var PYTEST_COLLECTION_HOOK_NAME_PATTERN = /\bpytest_collection_modifyitems\b/;
12377
12891
  var PYTEST_HOOK_CONDITIONAL_PATTERN = /^(?:if|elif|else|while|try|except|finally|with|match|case|return|break|continue|yield)\b|\bskipif\b|\bif\b/;
@@ -13904,11 +14418,39 @@ function findPytestRunWideSkips(repoPath, repoFiles) {
13904
14418
  const source = pythonSourceLines(text);
13905
14419
  if (source.masked.some((code) => PYTEST_COLLECT_IGNORE_PATTERN.test(code))) ignoresCollection = true;
13906
14420
  let last = -1;
14421
+ let blocked = false;
14422
+ const classHooks = [];
14423
+ const blocks = [];
14424
+ const openHooks = [];
13907
14425
  for (const { first, last: final } of pythonLogicalLines(source)) {
13908
- if (PYTEST_COLLECTION_HOOK_PATTERN.test(source.masked[first] ?? "")) {
13909
- last = first;
14426
+ const indent = pythonLineIndent(source, first);
14427
+ const opening = source.masked[first] ?? "";
14428
+ if (indent !== -1) {
14429
+ while (blocks.length > 0 && (blocks[blocks.length - 1]?.indent ?? 0) >= indent) {
14430
+ if (blocks.pop()?.hook === true) openHooks.pop();
14431
+ }
14432
+ const enclosing = blocks[blocks.length - 1]?.className;
14433
+ if (indent > 0 && enclosing !== void 0 && PYTEST_CLASS_COLLECTION_HOOK_PATTERN.test(opening)) {
14434
+ classHooks.push({ line: first, className: enclosing, touches: false });
14435
+ openHooks.push(classHooks.length - 1);
14436
+ blocks.push({ indent, hook: true });
14437
+ } else {
14438
+ let logical = "";
14439
+ for (let line = first; line <= final; line += 1) logical += `${source.masked[line] ?? ""}
14440
+ `;
14441
+ const open = openHooks[openHooks.length - 1];
14442
+ if (open !== void 0 && /\bitems\b/.test(logical)) classHooks[open].touches = true;
14443
+ if (logical.trimEnd().endsWith(":")) {
14444
+ const className = /^[ \t]*class[ \t]+([A-Za-z_]\w*)/.exec(opening)?.[1];
14445
+ blocks.push({ indent, hook: false, ...className === void 0 ? {} : { className } });
14446
+ }
14447
+ }
14448
+ }
14449
+ if (PYTEST_COLLECTION_HOOK_PATTERN.test(opening)) {
14450
+ if (!blocked) last = first;
13910
14451
  continue;
13911
14452
  }
14453
+ if (blocked) continue;
13912
14454
  let named = false;
13913
14455
  let code = "";
13914
14456
  for (let line = first; line <= final && !named; line += 1) {
@@ -13923,13 +14465,25 @@ function findPytestRunWideSkips(repoPath, repoFiles) {
13923
14465
  }
13924
14466
  if (named) {
13925
14467
  last = -1;
13926
- break;
14468
+ blocked = true;
13927
14469
  }
13928
14470
  }
14471
+ const scope = dirname2(file);
14472
+ for (const hook of classHooks) {
14473
+ if (!hook.touches) continue;
14474
+ const reason = `${hook.className}.${PYTEST_COLLECTION_HOOK_NAME}`;
14475
+ found.push({
14476
+ path: file,
14477
+ line: hook.line + 1,
14478
+ scope: scope === "." ? "" : scope,
14479
+ kind: "unread",
14480
+ unread: reason,
14481
+ withheld: `${A1_HOOK_NOT_READ}\`${reason}\` in ${a1NotePath(file)}`
14482
+ });
14483
+ }
13929
14484
  if (last === -1) continue;
13930
14485
  const reading = readPytestCollectionHook(source, last, builtinsNamedIn);
13931
14486
  if (reading === void 0) continue;
13932
- const scope = dirname2(file);
13933
14487
  const where = { path: file, line: last + 1, scope: scope === "." ? "" : scope };
13934
14488
  if (reading.kind === "not read") {
13935
14489
  const because = reading.builtins === void 0 ? "" : ` calls a builtin, ${reading.builtins}`;
@@ -23731,7 +24285,7 @@ function buildWitanCliSummary(report) {
23731
24285
  // src/scan.ts
23732
24286
  function runCejelScan(options) {
23733
24287
  const identity = deriveProductIdentity(options.repoPath, options.productName);
23734
- const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
24288
+ const generatedAt = options.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
23735
24289
  const report = scoreRepoWithPublicCejel({
23736
24290
  productSlug: identity.productSlug,
23737
24291
  productDisplayName: options.productDisplayName ?? identity.productDisplayName,
@@ -23782,6 +24336,7 @@ export {
23782
24336
  formatVerdictCapSentence,
23783
24337
  buildVerdictExplanation,
23784
24338
  buildRelyingPartySummary,
24339
+ METRIC_DERIVED_FINDING_PATTERN,
23785
24340
  buildPlainSummary,
23786
24341
  renderWitanHtmlReport,
23787
24342
  renderWitanBadgeEndpoint,