@stll/oxlint-config 0.12.1 → 0.12.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.
@@ -645,7 +645,7 @@ const generateCloudSetup = ({ policy, cloud, nodeVersion }) => {
645
645
  };
646
646
  //#endregion
647
647
  //#region src/toolchain-container-inputs.ts
648
- const record$6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
648
+ const record$7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
649
649
  /** Filename class recognized by Dependabot's Docker Compose fetcher. */
650
650
  const isComposeDefinitionPath = (file) => /^\.?(?:docker-)?compose(?:-[\w]+)?(?:\.[\w-]+)?\.ya?ml$/i.test(file.split("/").at(-1) ?? "");
651
651
  /** Candidate YAML files are classified by their document shape after reading. */
@@ -692,11 +692,11 @@ const kubernetesPodSpecPaths = {
692
692
  };
693
693
  /** Shared semantic image enumeration for runtime checks and update-root generation. */
694
694
  const containerDocumentImages = (document, file) => {
695
- if (!record$6(document)) return void 0;
695
+ if (!record$7(document)) return void 0;
696
696
  const services = document["services"];
697
- if (isComposeDefinitionPath(file) && record$6(services)) {
697
+ if (isComposeDefinitionPath(file) && record$7(services)) {
698
698
  const images = [];
699
- for (const [name, service] of Object.entries(services)) if (record$6(service) && service["image"] !== void 0) images.push({
699
+ for (const [name, service] of Object.entries(services)) if (record$7(service) && service["image"] !== void 0) images.push({
700
700
  image: service["image"],
701
701
  path: [
702
702
  "services",
@@ -717,7 +717,7 @@ const containerDocumentImages = (document, file) => {
717
717
  visited.add(resource);
718
718
  if (typeof resource["apiVersion"] !== "string" || typeof resource["kind"] !== "string") return;
719
719
  if (resource["kind"] === "List" && Array.isArray(resource["items"])) {
720
- for (const [index, item] of resource["items"].entries()) if (record$6(item)) walkResource(item, [
720
+ for (const [index, item] of resource["items"].entries()) if (record$7(item)) walkResource(item, [
721
721
  ...prefix,
722
722
  "items",
723
723
  index
@@ -727,8 +727,8 @@ const containerDocumentImages = (document, file) => {
727
727
  const specPath = Object.entries(kubernetesPodSpecPaths).find(([kind]) => kind === resource["kind"])?.[1];
728
728
  if (specPath === void 0) return;
729
729
  let spec = resource;
730
- for (const key of specPath) spec = record$6(spec) ? spec[key] : void 0;
731
- if (!record$6(spec)) return;
730
+ for (const key of specPath) spec = record$7(spec) ? spec[key] : void 0;
731
+ if (!record$7(spec)) return;
732
732
  for (const key of [
733
733
  "containers",
734
734
  "initContainers",
@@ -736,7 +736,7 @@ const containerDocumentImages = (document, file) => {
736
736
  ]) {
737
737
  const containers = spec[key];
738
738
  if (!Array.isArray(containers)) continue;
739
- for (const [index, container] of containers.entries()) if (record$6(container) && container["image"] !== void 0) images.push({
739
+ for (const [index, container] of containers.entries()) if (record$7(container) && container["image"] !== void 0) images.push({
740
740
  image: container["image"],
741
741
  path: [
742
742
  ...prefix,
@@ -839,7 +839,7 @@ const workspaceContains = ({ directory, file, patterns, rootMembership }) => {
839
839
  //#endregion
840
840
  //#region src/toolchain-dependabot.ts
841
841
  const dependabotRules = ["dependabot-policy"];
842
- const record$5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
842
+ const record$6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
843
843
  const strings = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
844
844
  const sameStrings = (value, expected) => {
845
845
  if (!strings(value) || value.length !== expected.length) return false;
@@ -861,8 +861,8 @@ const workspaceTable = (parsed, ecosystem) => {
861
861
  case "cargo": return parsed["workspace"];
862
862
  case "uv": {
863
863
  const tool = parsed["tool"];
864
- const uv = record$5(tool) ? tool["uv"] : void 0;
865
- return record$5(uv) ? uv["workspace"] : void 0;
864
+ const uv = record$6(tool) ? tool["uv"] : void 0;
865
+ return record$6(uv) ? uv["workspace"] : void 0;
866
866
  }
867
867
  default: throw new Error("unknown workspace ecosystem", { cause: ecosystem });
868
868
  }
@@ -882,7 +882,7 @@ const workspaceRoots = (files, ecosystem) => {
882
882
  const document = parseDocument(text);
883
883
  if (document.errors.length > 0) throw new Error(`${workspaceFile}:1: invalid pnpm workspace YAML`);
884
884
  const parsed = document.toJS({ maxAliasCount: 100 });
885
- if (!record$5(parsed)) throw new Error(`${workspaceFile}:1: pnpm workspace must contain an object`);
885
+ if (!record$6(parsed)) throw new Error(`${workspaceFile}:1: pnpm workspace must contain an object`);
886
886
  const patterns = parsed["packages"] ?? [];
887
887
  if (!strings(patterns)) throw new Error(`${workspaceFile}:1: pnpm packages must be a string array`);
888
888
  workspaces.set(directory, patterns.map((pattern) => pattern.startsWith("!") ? `!${path.posix.normalize(pattern.slice(1))}` : path.posix.normalize(pattern)));
@@ -894,13 +894,13 @@ const workspaceRoots = (files, ecosystem) => {
894
894
  }
895
895
  try {
896
896
  const parsed = ecosystem === "npm" ? JSON.parse(files[file] ?? "") : parse(files[file] ?? "");
897
- if (!record$5(parsed)) continue;
897
+ if (!record$6(parsed)) continue;
898
898
  if (ecosystem === "npm" && typeof parsed["packageManager"] === "string" && parsed["packageManager"].startsWith("pnpm@")) continue;
899
899
  const value = workspaceTable(parsed, ecosystem);
900
- const patterns = record$5(value) ? value[ecosystem === "npm" ? "packages" : "members"] : value;
901
- const members = ecosystem === "cargo" && record$5(value) && patterns === void 0 ? [] : patterns;
900
+ const patterns = record$6(value) ? value[ecosystem === "npm" ? "packages" : "members"] : value;
901
+ const members = ecosystem === "cargo" && record$6(value) && patterns === void 0 ? [] : patterns;
902
902
  if (strings(members)) {
903
- const excluded = ecosystem !== "npm" && record$5(value) && strings(value["exclude"]) ? value["exclude"].map((pattern) => `!${pattern}`) : [];
903
+ const excluded = ecosystem !== "npm" && record$6(value) && strings(value["exclude"]) ? value["exclude"].map((pattern) => `!${pattern}`) : [];
904
904
  workspaces.set(path.posix.dirname(file), [...members, ...excluded]);
905
905
  }
906
906
  } catch {}
@@ -951,11 +951,11 @@ const workspaceRoots = (files, ecosystem) => {
951
951
  for (const directory of memberDirectories) try {
952
952
  const parsed = parse(files[path.posix.join(directory, "Cargo.toml")] ?? "");
953
953
  const workspace = parse(files[path.posix.join(root, "Cargo.toml")] ?? "")["workspace"];
954
- const inherited = record$5(workspace) ? workspace["dependencies"] : void 0;
954
+ const inherited = record$6(workspace) ? workspace["dependencies"] : void 0;
955
955
  const dependencyOwners = [parsed];
956
956
  const targets = parsed["target"];
957
- if (record$5(targets)) {
958
- for (const target of Object.values(targets)) if (record$5(target)) dependencyOwners.push(target);
957
+ if (record$6(targets)) {
958
+ for (const target of Object.values(targets)) if (record$6(target)) dependencyOwners.push(target);
959
959
  }
960
960
  for (const owner of dependencyOwners) for (const key of [
961
961
  "dependencies",
@@ -963,12 +963,12 @@ const workspaceRoots = (files, ecosystem) => {
963
963
  "build-dependencies"
964
964
  ]) {
965
965
  const dependencies = owner[key];
966
- if (!record$5(dependencies)) continue;
966
+ if (!record$6(dependencies)) continue;
967
967
  for (const [name, dependency] of Object.entries(dependencies)) {
968
- if (!record$5(dependency)) continue;
968
+ if (!record$6(dependency)) continue;
969
969
  const isInherited = dependency["workspace"] === true;
970
- const resolved = isInherited && record$5(inherited) ? inherited[name] : dependency;
971
- if (!record$5(resolved) || typeof resolved["path"] !== "string") continue;
970
+ const resolved = isInherited && record$6(inherited) ? inherited[name] : dependency;
971
+ if (!record$6(resolved) || typeof resolved["path"] !== "string") continue;
972
972
  if (path.posix.isAbsolute(resolved["path"]) || /\\|\$|^[A-Za-z]:/.test(resolved["path"])) continue;
973
973
  const target = path.posix.normalize(path.posix.join(isInherited ? root : directory, resolved["path"]));
974
974
  const relative = path.posix.relative(root, target);
@@ -996,7 +996,7 @@ const javascriptEcosystem = (directory, files) => {
996
996
  if (files[path.posix.join(root, "bun.lock")] !== void 0) return "bun";
997
997
  try {
998
998
  const manifest = JSON.parse(files[path.posix.join(root, "package.json")] ?? "{}");
999
- if (record$5(manifest) && typeof manifest["packageManager"] === "string" && manifest["packageManager"].startsWith("bun@")) return "bun";
999
+ if (record$6(manifest) && typeof manifest["packageManager"] === "string" && manifest["packageManager"].startsWith("bun@")) return "bun";
1000
1000
  } catch {}
1001
1001
  return "npm";
1002
1002
  };
@@ -1008,7 +1008,7 @@ const pythonEcosystem = (file, files) => {
1008
1008
  const pyproject = files[path.posix.join(root, "pyproject.toml")];
1009
1009
  if (pyproject !== void 0) try {
1010
1010
  const tool = parse(pyproject)["tool"];
1011
- if (record$5(tool) && record$5(tool["uv"])) return "uv";
1011
+ if (record$6(tool) && record$6(tool["uv"])) return "uv";
1012
1012
  } catch {}
1013
1013
  return "pip";
1014
1014
  };
@@ -1079,7 +1079,7 @@ const checkDependabot = ({ files, policy }) => {
1079
1079
  add("cannot resolve Dependabot YAML aliases");
1080
1080
  return diagnostics;
1081
1081
  }
1082
- if (!record$5(parsed) || parsed["version"] !== 2 || !Array.isArray(parsed["updates"])) {
1082
+ if (!record$6(parsed) || parsed["version"] !== 2 || !Array.isArray(parsed["updates"])) {
1083
1083
  add("Dependabot must declare version: 2 and an updates array");
1084
1084
  return diagnostics;
1085
1085
  }
@@ -1092,7 +1092,7 @@ const checkDependabot = ({ files, policy }) => {
1092
1092
  });
1093
1093
  if (legacyDocument.errors.length === 0) legacy = legacyDocument.toJS({ maxAliasCount: 100 });
1094
1094
  } catch {}
1095
- const legacyUpdates = record$5(legacy) && Array.isArray(legacy["updates"]) ? legacy["updates"] : [];
1095
+ const legacyUpdates = record$6(legacy) && Array.isArray(legacy["updates"]) ? legacy["updates"] : [];
1096
1096
  const rootClaims = /* @__PURE__ */ new Map();
1097
1097
  const directoryClaims = /* @__PURE__ */ new Map();
1098
1098
  for (const [index, update] of parsed["updates"].entries()) {
@@ -1106,7 +1106,7 @@ const checkDependabot = ({ files, policy }) => {
1106
1106
  return offset === void 0 ? 1 : lineCounter.linePos(offset).line;
1107
1107
  };
1108
1108
  const report = (key, message) => add(message, lineOf(key));
1109
- if (!record$5(update) || typeof update["package-ecosystem"] !== "string") {
1109
+ if (!record$6(update) || typeof update["package-ecosystem"] !== "string") {
1110
1110
  report("package-ecosystem", "each Dependabot update must declare a package ecosystem");
1111
1111
  continue;
1112
1112
  }
@@ -1140,26 +1140,75 @@ const checkDependabot = ({ files, policy }) => {
1140
1140
  covered.set(ecosystem, matches);
1141
1141
  const schedule = update["schedule"];
1142
1142
  const legacyUpdate = legacyUpdates.at(index);
1143
- const legacySchedule = record$5(legacyUpdate) ? legacyUpdate["schedule"] : void 0;
1144
- if (record$5(schedule) && schedule["time"] === policy.schedule.time && (!record$5(legacySchedule) || legacySchedule["time"] !== policy.schedule.time)) report("schedule", `${ecosystem}: quote schedule time to preserve its string value in YAML 1.1`);
1145
- if (!record$5(schedule) || Object.keys(schedule).length !== Object.keys(policy.schedule).length || Object.entries(policy.schedule).some(([key, value]) => schedule[key] !== value)) report("schedule", `${ecosystem}: schedule must match the shared policy`);
1143
+ const legacySchedule = record$6(legacyUpdate) ? legacyUpdate["schedule"] : void 0;
1144
+ if (record$6(schedule) && schedule["time"] === policy.schedule.time && (!record$6(legacySchedule) || legacySchedule["time"] !== policy.schedule.time)) report("schedule", `${ecosystem}: quote schedule time to preserve its string value in YAML 1.1`);
1145
+ if (!record$6(schedule) || Object.keys(schedule).length !== Object.keys(policy.schedule).length || Object.entries(policy.schedule).some(([key, value]) => schedule[key] !== value)) report("schedule", `${ecosystem}: schedule must match the shared policy`);
1146
1146
  const cooldown = update["cooldown"];
1147
- if (!record$5(cooldown) || Object.keys(cooldown).length !== 1 || cooldown["default-days"] !== policy.cooldown.defaultDays) report("cooldown", `${ecosystem}: cooldown must contain only default-days: ${policy.cooldown.defaultDays}`);
1147
+ if (!record$6(cooldown) || Object.keys(cooldown).length !== 1 || cooldown["default-days"] !== policy.cooldown.defaultDays) report("cooldown", `${ecosystem}: cooldown must contain only default-days: ${policy.cooldown.defaultDays}`);
1148
1148
  const groups = update["groups"];
1149
- if (!record$5(groups) || Object.keys(groups).length !== Object.keys(policy.groups).length || Object.entries(policy.groups).some(([name, expected]) => {
1149
+ if (!record$6(groups) || Object.keys(groups).length !== Object.keys(policy.groups).length || Object.entries(policy.groups).some(([name, expected]) => {
1150
1150
  const group = groups[name];
1151
- return !record$5(group) || Object.keys(group).length !== 2 || !sameStrings(group["patterns"], expected.patterns) || !sameStrings(group["update-types"], expected.updateTypes);
1151
+ return !record$6(group) || Object.keys(group).length !== 2 || !sameStrings(group["patterns"], expected.patterns) || !sameStrings(group["update-types"], expected.updateTypes);
1152
1152
  })) report("groups", `${ecosystem}: groups must match the shared policy`);
1153
1153
  const ignored = ignoredFor(ecosystem, policy);
1154
1154
  const ignore = update["ignore"];
1155
1155
  if (ignore !== void 0 && (!Array.isArray(ignore) || ignore.length === 0)) report("ignore", `${ecosystem}: omit ignore or provide a nonempty array`);
1156
- if (Array.isArray(ignore) && ignore.some((entry) => !record$5(entry) || typeof entry["dependency-name"] !== "string" || !ignored.includes(entry["dependency-name"]))) report("ignore", `${ecosystem}: ignore only packages owned by the shared policy`);
1157
- for (const name of ignored) if (!Array.isArray(ignore) || !ignore.some((entry) => record$5(entry) && entry["dependency-name"] === name && Object.keys(entry).length === 1)) report("ignore", `${ecosystem}: ignore all updates for shared pin ${name}`);
1156
+ if (Array.isArray(ignore) && ignore.some((entry) => !record$6(entry) || typeof entry["dependency-name"] !== "string" || !ignored.includes(entry["dependency-name"]))) report("ignore", `${ecosystem}: ignore only packages owned by the shared policy`);
1157
+ for (const name of ignored) if (!Array.isArray(ignore) || !ignore.some((entry) => record$6(entry) && entry["dependency-name"] === name && Object.keys(entry).length === 1)) report("ignore", `${ecosystem}: ignore all updates for shared pin ${name}`);
1158
1158
  }
1159
1159
  for (const [ecosystem, directories] of roots) for (const directory of directories) if (!covered.get(ecosystem)?.has(directory)) add(`add a ${ecosystem} update entry covering ${directory}`);
1160
1160
  return diagnostics;
1161
1161
  };
1162
1162
  //#endregion
1163
+ //#region src/toolchain-dynamic-selectors.ts
1164
+ const locatorValidators = {
1165
+ image: /^(?:jobs\.[\w-]+\.(?:container|services\.[\w-]+)|services\.[\w.-]+\.image)$/,
1166
+ "bun-source": /^(?:jobs\.[\w-]+|runs)\.steps\.[\w-]+$/
1167
+ };
1168
+ const knownKind = (value) => typeof value === "string" && Object.hasOwn(locatorValidators, value);
1169
+ const record$5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1170
+ /** One reviewed declaration identifies one unresolved selector, never a rule. */
1171
+ const parseDynamicSelectors = (input) => {
1172
+ if (input === void 0) return [];
1173
+ if (!Array.isArray(input)) throw new Error("dynamicSelectors must be an array");
1174
+ const result = [];
1175
+ const keys = /* @__PURE__ */ new Set();
1176
+ for (const entry of input) {
1177
+ if (!record$5(entry) || Object.keys(entry).some((key) => ![
1178
+ "path",
1179
+ "kind",
1180
+ "reason",
1181
+ "at",
1182
+ "line"
1183
+ ].includes(key)) || typeof entry["path"] !== "string" || entry["path"] === "" || /[\\:$*?]|[^\S ]/.test(entry["path"]) || entry["path"].split("/").some((part) => part === "" || part === "." || part === "..") || typeof entry["reason"] !== "string" || entry["reason"].trim() === "") throw new Error("each dynamic selector requires an exact repository path, known kind and nonempty reason");
1184
+ const kind = entry["kind"];
1185
+ if (!knownKind(kind)) throw new Error("unknown dynamic selector kind");
1186
+ let selector;
1187
+ if (Object.hasOwn(entry, "line") && !Object.hasOwn(entry, "at") && typeof entry["line"] === "number" && Number.isSafeInteger(entry["line"]) && entry["line"] > 0) selector = {
1188
+ path: entry["path"],
1189
+ kind,
1190
+ reason: entry["reason"],
1191
+ line: entry["line"]
1192
+ };
1193
+ else if (!Object.hasOwn(entry, "line") && typeof entry["at"] === "string" && locatorValidators[kind].test(entry["at"])) selector = {
1194
+ path: entry["path"],
1195
+ kind,
1196
+ reason: entry["reason"],
1197
+ at: entry["at"]
1198
+ };
1199
+ else throw new Error("dynamic selector requires one positive line or a kind-specific job/service/step ID locator");
1200
+ const key = JSON.stringify([
1201
+ selector.path,
1202
+ selector.kind,
1203
+ selector.at ?? selector.line
1204
+ ]);
1205
+ if (keys.has(key)) throw new Error("dynamic selector locations must be unique");
1206
+ keys.add(key);
1207
+ result.push(selector);
1208
+ }
1209
+ return result;
1210
+ };
1211
+ //#endregion
1163
1212
  //#region src/toolchain-engine-floors.ts
1164
1213
  const record$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1165
1214
  const repositoryPath = (value) => typeof value === "string" && value !== "" && !/^[A-Za-z]:/.test(value) && !/[\\\s]/.test(value) && !Array.from(value).some((character) => character.charCodeAt(0) < 32) && value.split("/").every((part) => part !== "" && part !== "." && part !== "..");
@@ -1971,7 +2020,7 @@ const pythonRequirementMatches = (requirement, version) => {
1971
2020
  });
1972
2021
  };
1973
2022
  /** Check runtime declarations only in the files that declare or configure them. */
1974
- const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, repository, onDelegated, engineFloors = [], onEngineFloor }) => {
2023
+ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, repository, onDelegated, dynamicSelectors = [], onDynamicSelector, engineFloors = [], onEngineFloor }) => {
1975
2024
  const diagnostics = [];
1976
2025
  const name = path.posix.basename(file);
1977
2026
  const lines = text.split(/\r?\n/);
@@ -1983,6 +2032,16 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
1983
2032
  message
1984
2033
  });
1985
2034
  };
2035
+ let currentLocation;
2036
+ const dynamic = (value) => typeof value === "string" && value.includes("$");
2037
+ const authorizeDynamic = (kind, line, at = currentLocation) => {
2038
+ const matches = dynamicSelectors.filter((entry) => entry.path === file && entry.kind === kind && (entry.at !== void 0 ? entry.at === at : entry.line === line));
2039
+ if (matches.length !== 1) return false;
2040
+ const entry = matches.at(0);
2041
+ if (entry === void 0) return false;
2042
+ onDynamicSelector?.(entry);
2043
+ return true;
2044
+ };
1986
2045
  const lineOf = (key) => Math.max(1, lines.findIndex((line) => line.includes(key)) + 1);
1987
2046
  const pin = ({ rule, value, expected, label, line = 1, selector }) => {
1988
2047
  let matches = value === expected;
@@ -2184,12 +2243,13 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2184
2243
  line: index + 1
2185
2244
  });
2186
2245
  });
2187
- const checkRuntimeImage = ({ image, line, label }) => {
2246
+ const checkRuntimeImage = ({ image, line, label, at }) => {
2188
2247
  const unknownRuntime = () => {
2248
+ if (dynamic(image) && authorizeDynamic("image", line, at)) return;
2189
2249
  add({
2190
2250
  rule: "runtime-docker",
2191
2251
  line,
2192
- message: `cannot determine ${label} runtime; use a static image or a reasoned runtime-docker opt-out`
2252
+ message: `cannot determine ${label} runtime; use a static image or a scoped dynamicSelectors declaration`
2193
2253
  });
2194
2254
  };
2195
2255
  if (typeof image !== "string") {
@@ -2418,6 +2478,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2418
2478
  checkRuntimeImage({
2419
2479
  image: entry.image,
2420
2480
  line,
2481
+ at: entry.path.join("."),
2421
2482
  label: classified?.ecosystem === "docker-compose" ? "Compose image" : "Kubernetes image"
2422
2483
  });
2423
2484
  }
@@ -2452,8 +2513,22 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2452
2513
  let checkoutBindings = [];
2453
2514
  let checkoutDestinations = /* @__PURE__ */ new Map();
2454
2515
  let unknownCheckoutDestination = false;
2516
+ let unreviewableCheckoutSource = false;
2455
2517
  const staticRepositoryPath = (value) => typeof value === "string" && value !== "" && !value.includes("${{") && !value.includes("\\") && !value.includes(":") && !path.posix.isAbsolute(value) && !value.split("/").includes("..");
2456
2518
  const normalizeCheckoutPath = (prefix) => path.posix.normalize(prefix).replace(/\/$/, "") || ".";
2519
+ const checkoutProvidersFor = (selector) => {
2520
+ if (selector === void 0) return checkoutBindings;
2521
+ let deepest;
2522
+ for (const entry of checkoutBindings) {
2523
+ if (entry.path !== "." && selector !== entry.path && !selector.startsWith(`${entry.path}/`)) continue;
2524
+ if (deepest === void 0 || entry.path !== "." && (deepest.path === "." || entry.path.length > deepest.path.length)) deepest = entry;
2525
+ }
2526
+ return checkoutBindings.filter((entry) => entry.path === deepest?.path);
2527
+ };
2528
+ const hasAmbiguousCheckoutWriters = (selector, providers) => {
2529
+ if (providers.length > 0) return providers.some((entry) => (checkoutDestinations.get(entry.path) ?? 0) > 1);
2530
+ return [...checkoutDestinations].some(([prefix, count]) => count > 1 && (selector === void 0 || prefix === "." || selector === prefix || selector.startsWith(`${prefix}/`)));
2531
+ };
2457
2532
  const checkReference = ({ value, tool, line }) => {
2458
2533
  const allowed = {
2459
2534
  node: [".node-version", ".nvmrc"],
@@ -2462,8 +2537,28 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2462
2537
  }[tool];
2463
2538
  const rule = tool === "bun" ? "bun-pins" : "runtime-workflow";
2464
2539
  const selector = staticRepositoryPath(value) ? path.posix.normalize(value) : void 0;
2465
- const binding = selector === void 0 ? void 0 : checkoutBindings.findLast((entry) => entry.path === "." || selector === entry.path || selector.startsWith(`${entry.path}/`));
2540
+ const providers = checkoutProvidersFor(selector);
2541
+ const binding = selector === void 0 ? void 0 : providers.at(-1);
2542
+ const dynamicSource = providers.some((entry) => entry.dynamicSource);
2543
+ const sparseFailure = providers.find((entry) => {
2544
+ if (entry.sparse.mode === "invalid") return true;
2545
+ if (entry.sparse.mode !== "files" && entry.sparse.mode !== "mixed") return false;
2546
+ if (selector === void 0) return true;
2547
+ const selectedFile = entry.path === "." ? selector : path.posix.relative(entry.path, selector);
2548
+ return !entry.sparse.paths.has(selectedFile);
2549
+ });
2550
+ if (sparseFailure !== void 0) {
2551
+ let missingFile = "the selected version file";
2552
+ if (selector !== void 0) missingFile = sparseFailure.path === "." ? selector : path.posix.relative(sparseFailure.path, selector);
2553
+ add({
2554
+ rule,
2555
+ line,
2556
+ message: `checkout sparse-checkout must explicitly list ${missingFile} without invalid literal patterns`
2557
+ });
2558
+ return;
2559
+ }
2466
2560
  if (selector === void 0) {
2561
+ if (tool === "bun" && dynamic(value) && !unreviewableCheckoutSource && authorizeDynamic("bun-source", line)) return;
2467
2562
  add({
2468
2563
  rule,
2469
2564
  line,
@@ -2471,7 +2566,8 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2471
2566
  });
2472
2567
  return;
2473
2568
  }
2474
- if (unknownCheckoutDestination || [...checkoutDestinations].some(([prefix, count]) => count > 1 && (prefix === "." || selector === prefix || selector.startsWith(`${prefix}/`)))) {
2569
+ if (unknownCheckoutDestination || hasAmbiguousCheckoutWriters(selector, providers)) {
2570
+ if (tool === "bun" && dynamicSource && !unreviewableCheckoutSource && authorizeDynamic("bun-source", line)) return;
2475
2571
  add({
2476
2572
  rule,
2477
2573
  line,
@@ -2480,6 +2576,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2480
2576
  return;
2481
2577
  }
2482
2578
  if (binding?.source === "untrusted") {
2579
+ if (tool === "bun" && dynamicSource && !unreviewableCheckoutSource && authorizeDynamic("bun-source", line)) return;
2483
2580
  add({
2484
2581
  rule,
2485
2582
  line: binding.line,
@@ -2496,13 +2593,15 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2496
2593
  });
2497
2594
  return;
2498
2595
  }
2499
- if (binding !== void 0 && (binding.sparse.mode === "invalid" || binding.sparse.mode === "files" && !binding.sparse.paths.has(target))) {
2500
- add({
2501
- rule,
2502
- line,
2503
- message: `checkout sparse-checkout must explicitly list ${target} without dynamic, glob, or negation patterns`
2504
- });
2505
- return;
2596
+ if (binding !== void 0 && (binding.sparse.mode === "dynamic" || binding.sparse.mode === "mixed")) {
2597
+ if (tool !== "bun" || unreviewableCheckoutSource || !authorizeDynamic("bun-source", line)) {
2598
+ add({
2599
+ rule,
2600
+ line,
2601
+ message: `checkout sparse-checkout must explicitly list ${target} without dynamic, glob, or negation patterns`
2602
+ });
2603
+ return;
2604
+ }
2506
2605
  }
2507
2606
  if (binding?.source === "delegated") {
2508
2607
  onDelegated?.({
@@ -2624,10 +2723,10 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2624
2723
  return inputs;
2625
2724
  };
2626
2725
  const getInput = (node, key) => inputsOf(node).get(key.toUpperCase());
2627
- const dynamicRefBody = (value) => {
2628
- if (typeof value !== "string" || !value.startsWith("${{")) return void 0;
2726
+ const expressionAt = (value, start) => {
2727
+ if (!value.startsWith("${{", start)) return void 0;
2629
2728
  let quote;
2630
- for (let index = 3; index < value.length; index++) {
2729
+ for (let index = start + 3; index < value.length; index++) {
2631
2730
  const character = value.at(index);
2632
2731
  if (quote !== void 0) {
2633
2732
  if (character === quote) if (quote === "'" && value.at(index + 1) === "'") index++;
@@ -2637,9 +2736,33 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2637
2736
  }
2638
2737
  if (character === "'" || character === "\"") quote = character;
2639
2738
  else if (value.startsWith("${{", index)) return void 0;
2640
- else if (character === "}" && value.at(index + 1) === "}") return index === value.length - 2 ? value.slice(3, index).trim() : void 0;
2739
+ else if (character === "}" && value.at(index + 1) === "}") return {
2740
+ body: value.slice(start + 3, index).trim(),
2741
+ end: index + 2
2742
+ };
2641
2743
  }
2642
2744
  };
2745
+ const dynamicRefBody = (value) => {
2746
+ if (typeof value !== "string") return void 0;
2747
+ const expression = expressionAt(value, 0);
2748
+ return expression?.end === value.length ? expression.body : void 0;
2749
+ };
2750
+ const sparseEntryIsValid = (entry) => {
2751
+ let literalPath = "";
2752
+ let position = 0;
2753
+ while (position < entry.length) {
2754
+ const start = entry.indexOf("${{", position);
2755
+ const literal = entry.slice(position, start < 0 ? entry.length : start);
2756
+ if (/[!$*?[\]\\:]/.test(literal) || literal.split("/").includes("..")) return false;
2757
+ literalPath += literal;
2758
+ if (start < 0) break;
2759
+ const expression = expressionAt(entry, start);
2760
+ if (expression === void 0 || expression.body === "") return false;
2761
+ literalPath += "expression";
2762
+ position = expression.end;
2763
+ }
2764
+ return staticRepositoryPath(literalPath);
2765
+ };
2643
2766
  const checkoutPrefixOf = (node) => {
2644
2767
  const destination = getInput(node, "path");
2645
2768
  if (destination === void 0) return ".";
@@ -2652,11 +2775,19 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2652
2775
  const value = getInput(node, "sparse-checkout");
2653
2776
  const cone = getInput(node, "sparse-checkout-cone-mode");
2654
2777
  if (!isScalar(value) || typeof value.value !== "string" || cone !== void 0 && (!isScalar(cone) || cone.value !== true && cone.value !== false && cone.value !== "true" && cone.value !== "false")) return { mode: "invalid" };
2655
- const paths = value.value.split(/\r?\n/).map((entry) => entry.trim()).filter((entry) => entry !== "");
2656
- if (paths.length === 0 || paths.some((entry) => !staticRepositoryPath(entry) || entry.startsWith("!") || /[*?[\]]/.test(entry))) return { mode: "invalid" };
2778
+ const paths = value.value.split(/\r?\n/).map((entry) => entry.trim().replace(/^\//, "")).filter((entry) => entry !== "");
2779
+ if (paths.length === 0 || paths.some((entry) => !sparseEntryIsValid(entry))) return { mode: "invalid" };
2780
+ const literalPaths = new Set(paths.filter((entry) => !dynamic(entry)));
2781
+ if (paths.some(dynamic)) {
2782
+ if (literalPaths.size === 0) return { mode: "dynamic" };
2783
+ return {
2784
+ mode: "mixed",
2785
+ paths: literalPaths
2786
+ };
2787
+ }
2657
2788
  return {
2658
2789
  mode: "files",
2659
- paths: new Set(paths)
2790
+ paths: literalPaths
2660
2791
  };
2661
2792
  };
2662
2793
  let currentEngineFloor;
@@ -2686,8 +2817,9 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2686
2817
  }
2687
2818
  if (value.startsWith("./")) {
2688
2819
  const selector = staticRepositoryPath(value) ? path.posix.normalize(value) : void 0;
2689
- const binding = selector === void 0 ? void 0 : checkoutBindings.findLast((entry) => entry.path === "." || selector === entry.path || selector.startsWith(`${entry.path}/`));
2690
- if (selector === void 0 || unknownCheckoutDestination || binding !== void 0 && binding.source !== "tracked" || [...checkoutDestinations].some(([prefix, count]) => count > 1 && (prefix === "." || selector === prefix || selector.startsWith(`${prefix}/`)))) add({
2820
+ const providers = checkoutProvidersFor(selector);
2821
+ const binding = selector === void 0 ? void 0 : providers.at(-1);
2822
+ if (selector === void 0 || unknownCheckoutDestination || binding !== void 0 && binding.source !== "tracked" || hasAmbiguousCheckoutWriters(selector, providers)) add({
2691
2823
  rule: "action-pins",
2692
2824
  line,
2693
2825
  message: "local actions require an unambiguous checkout of the current source snapshot"
@@ -2743,24 +2875,55 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2743
2875
  const checkoutRepository = getInput(node, "repository");
2744
2876
  const checkoutRef = getInput(node, "ref");
2745
2877
  const prefix = checkoutPrefixOf(node);
2878
+ const clean = getInput(node, "clean");
2879
+ const cleanIsResolved = clean === void 0 || isScalar(clean) && !dynamic(clean.value);
2746
2880
  let repo;
2747
2881
  if (checkoutRepository !== void 0) repo = isScalar(checkoutRepository) ? checkoutRepository.value : null;
2748
2882
  const self = repo === void 0 || repo === "${{ github.repository }}" || typeof repo === "string" && repository !== void 0 && repo.toLowerCase() === repository.toLowerCase();
2749
2883
  const workflowSource = repo === "${{ job.workflow_repository }}" && isScalar(checkoutRef) && checkoutRef.value === "${{ job.workflow_sha }}";
2750
- const currentSource = self && checkoutRef === void 0;
2751
- const refValue = isScalar(checkoutRef) ? checkoutRef.value : void 0;
2884
+ const triggers = getNode(document.contents, "on");
2885
+ const callerContext = (() => {
2886
+ if (isMap(triggers)) return [...keysOf(triggers)];
2887
+ if (isSeq(triggers)) return triggers.items.map((event) => isScalar(event) ? event.value : void 0);
2888
+ if (isScalar(triggers)) return [triggers.value];
2889
+ return [];
2890
+ })().includes("workflow_call");
2891
+ let refValue = isScalar(checkoutRef) ? checkoutRef.value : void 0;
2892
+ if (callerContext && (repo === void 0 || repo === "${{ github.repository }}") && checkoutRef === void 0) refValue = "${{ github.sha }}";
2893
+ const currentRef = dynamicRefBody(refValue);
2894
+ const currentSource = self && !callerContext && (checkoutRef === void 0 || currentRef === "github.sha");
2752
2895
  const dynamicRef = dynamicRefBody(refValue);
2753
- const delegatedSource = (self || repo === "${{ job.workflow_repository }}") && dynamicRef !== void 0 && dynamicRef !== "" && !/^(?:'(?:[^']|'')*'|"(?:[^"\\]|\\.)*"|(?:true|false|null)|-?(?:0x[\da-f]+|(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?))$/i.test(dynamicRef) && !/^(?:github\s*(?:\.\s*sha|\[\s*['"]sha['"]\s*\])|job\s*(?:\.\s*workflow_sha|\[\s*['"]workflow_sha['"]\s*\]))$/i.test(dynamicRef);
2896
+ const delegatedSource = !currentSource && (self || repo === "${{ job.workflow_repository }}") && dynamicRef !== void 0 && dynamicRef !== "" && !/^(?:'(?:[^']|'')*'|"(?:[^"\\]|\\.)*"|(?:true|false|null)|-?(?:0x[\da-f]+|(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?))$/i.test(dynamicRef) && (!/^(?:github\s*(?:\.\s*sha|\[\s*['"]sha['"]\s*\])|job\s*(?:\.\s*workflow_sha|\[\s*['"]workflow_sha['"]\s*\]))$/i.test(dynamicRef) || callerContext && currentRef === "github.sha");
2897
+ const validSource = cleanIsResolved && ref === approved?.sha && sourceMap({
2898
+ node,
2899
+ key: "if"
2900
+ }) === void 0 && sourceMap({
2901
+ node,
2902
+ key: "continue-on-error"
2903
+ }) === void 0 && (currentSource || workflowSource || delegatedSource);
2904
+ if (!validSource) unreviewableCheckoutSource = true;
2754
2905
  if (!staticRepositoryPath(prefix)) checkoutBindings.push({
2755
2906
  path: ".",
2907
+ dynamicSource: validSource && dynamic(prefix),
2756
2908
  source: "untrusted",
2757
2909
  reason: "checkout destination must be a static repository path",
2758
2910
  line,
2759
- sparse: { mode: "invalid" }
2911
+ sparse: sparseCheckoutOf(node)
2760
2912
  });
2761
2913
  else {
2762
2914
  const destination = normalizeCheckoutPath(prefix);
2763
- const trustedWriter = ref === approved?.sha && sourceMap({
2915
+ checkoutBindings = checkoutBindings.map((entry) => {
2916
+ if (entry.path === destination || destination !== "." && !entry.path.startsWith(`${destination}/`)) return entry;
2917
+ return {
2918
+ path: entry.path,
2919
+ sparse: { mode: "all" },
2920
+ dynamicSource: false,
2921
+ source: "untrusted",
2922
+ reason: "a later ancestor checkout invalidates this descendant destination",
2923
+ line
2924
+ };
2925
+ });
2926
+ const trustedWriter = cleanIsResolved && ref === approved?.sha && sourceMap({
2764
2927
  node,
2765
2928
  key: "if"
2766
2929
  }) === void 0 && sourceMap({
@@ -2769,18 +2932,21 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2769
2932
  }) === void 0 && checkoutDestinations.get(destination) === 1 && !unknownCheckoutDestination;
2770
2933
  if (trustedWriter && delegatedSource && typeof refValue === "string") checkoutBindings.push({
2771
2934
  path: destination,
2935
+ dynamicSource: validSource && delegatedSource,
2772
2936
  sparse: sparseCheckoutOf(node),
2773
2937
  source: "delegated",
2774
2938
  ref: refValue
2775
2939
  });
2776
2940
  else if (trustedWriter && (workflowSource || currentSource)) checkoutBindings.push({
2777
2941
  path: destination,
2942
+ dynamicSource: validSource && delegatedSource,
2778
2943
  sparse: sparseCheckoutOf(node),
2779
2944
  source: "tracked"
2780
2945
  });
2781
2946
  else {
2782
2947
  let reason = "checkout does not select a trusted repository source snapshot";
2783
- if (ref !== approved?.sha) reason = "actions/checkout must use the approved action SHA";
2948
+ if (!cleanIsResolved) reason = "checkout clean must be statically resolved";
2949
+ else if (ref !== approved?.sha) reason = "actions/checkout must use the approved action SHA";
2784
2950
  else if (sourceMap({
2785
2951
  node,
2786
2952
  key: "if"
@@ -2791,6 +2957,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2791
2957
  else if (checkoutDestinations.get(destination) !== 1 || unknownCheckoutDestination) reason = "checkout destination must have exactly one static writer";
2792
2958
  checkoutBindings.push({
2793
2959
  path: destination,
2960
+ dynamicSource: validSource && delegatedSource,
2794
2961
  sparse: sparseCheckoutOf(node),
2795
2962
  source: "untrusted",
2796
2963
  reason,
@@ -2839,21 +3006,23 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2839
3006
  line: reference === void 0 ? line : nodeLine(reference)
2840
3007
  });
2841
3008
  };
2842
- const checkContainer = (node) => {
3009
+ const checkContainer = (node, at) => {
2843
3010
  node = resolveNode(node);
2844
3011
  if (node === void 0) return;
2845
3012
  const image = isMap(node) ? getNode(node, "image") : node;
2846
3013
  checkRuntimeImage({
2847
3014
  image: isScalar(image) ? image.value : void 0,
2848
3015
  line: nodeLine(image ?? node),
2849
- label: "container image"
3016
+ label: "container image",
3017
+ at
2850
3018
  });
2851
3019
  };
2852
- const checkSteps = (node, floor) => {
3020
+ const checkSteps = (node, locationPrefix, floor) => {
2853
3021
  currentEngineFloor = void 0;
2854
3022
  checkoutBindings = [];
2855
3023
  checkoutDestinations = /* @__PURE__ */ new Map();
2856
3024
  unknownCheckoutDestination = false;
3025
+ unreviewableCheckoutSource = false;
2857
3026
  node = resolveNode(node);
2858
3027
  if (!isSeq(node)) return;
2859
3028
  for (const step of node.items) {
@@ -2869,9 +3038,14 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2869
3038
  }
2870
3039
  currentEngineFloor = floor;
2871
3040
  try {
2872
- for (const step of node.items) checkAction(step);
3041
+ for (const step of node.items) {
3042
+ const id = getNode(step, "id");
3043
+ currentLocation = isScalar(id) && typeof id.value === "string" ? `${locationPrefix}.steps.${id.value}` : void 0;
3044
+ checkAction(step);
3045
+ }
2873
3046
  } finally {
2874
3047
  currentEngineFloor = void 0;
3048
+ currentLocation = void 0;
2875
3049
  }
2876
3050
  };
2877
3051
  try {
@@ -2885,15 +3059,15 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2885
3059
  currentEngineFloor = void 0;
2886
3060
  const job = getNode(jobs, key);
2887
3061
  checkAction(job);
2888
- checkContainer(getNode(job, "container"));
3062
+ checkContainer(getNode(job, "container"), `jobs.${key}.container`);
2889
3063
  const services = getNode(job, "services");
2890
- if (isMap(services)) for (const service of keysOf(services)) checkContainer(getNode(services, service));
2891
- checkSteps(getNode(job, "steps"), engineFloors.find((entry) => entry.job === key));
3064
+ if (isMap(services)) for (const service of keysOf(services)) checkContainer(getNode(services, service), `jobs.${key}.services.${service}`);
3065
+ checkSteps(getNode(job, "steps"), `jobs.${key}`, engineFloors.find((entry) => entry.job === key));
2892
3066
  }
2893
3067
  } else {
2894
3068
  const runs = getNode(document.contents, "runs");
2895
3069
  const using = getNode(runs, "using");
2896
- if (isScalar(using) && using.value === "composite") checkSteps(getNode(runs, "steps"));
3070
+ if (isScalar(using) && using.value === "composite") checkSteps(getNode(runs, "steps"), "runs");
2897
3071
  if (isScalar(using) && using.value === "docker") {
2898
3072
  const image = getNode(runs, "image");
2899
3073
  const value = isScalar(image) ? image.value : void 0;
@@ -3122,7 +3296,7 @@ const record = (value) => typeof value === "object" && value !== null && !Array.
3122
3296
  const array = (value) => Array.isArray(value);
3123
3297
  /** Repository-owned declarations are explicit; malformed decisions fail closed. */
3124
3298
  const parseToolchainConfiguration = (input) => {
3125
- if (!record(input) || Object.keys(input).some((key) => key !== "optOuts" && key !== "cloud" && key !== "engineFloors") || input["optOuts"] !== void 0 && !array(input["optOuts"])) throw new Error("stll-toolchain.json accepts only optOuts, cloud and engineFloors declarations");
3299
+ if (!record(input) || Object.keys(input).some((key) => key !== "optOuts" && key !== "cloud" && key !== "engineFloors" && key !== "dynamicSelectors") || input["optOuts"] !== void 0 && !array(input["optOuts"])) throw new Error("stll-toolchain.json accepts only optOuts, cloud, engineFloors and dynamicSelectors declarations");
3126
3300
  const disabled = /* @__PURE__ */ new Set();
3127
3301
  const optOuts = input["optOuts"] ?? [];
3128
3302
  if (!array(optOuts)) throw new Error("optOuts must be an array");
@@ -3135,7 +3309,8 @@ const parseToolchainConfiguration = (input) => {
3135
3309
  return {
3136
3310
  disabled,
3137
3311
  cloud: parseCloudSetup(input["cloud"]),
3138
- engineFloors: parseEngineFloors(input["engineFloors"])
3312
+ engineFloors: parseEngineFloors(input["engineFloors"]),
3313
+ dynamicSelectors: parseDynamicSelectors(input["dynamicSelectors"])
3139
3314
  };
3140
3315
  };
3141
3316
  /** Read one tracked configuration snapshot for guards and policy generation. */
@@ -3201,10 +3376,12 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
3201
3376
  let disabled = /* @__PURE__ */ new Set();
3202
3377
  let cloud;
3203
3378
  const engineFloors = [];
3379
+ let dynamicSelectors = [];
3204
3380
  if (files["stll-toolchain.json"] !== void 0) try {
3205
3381
  const configuration = parseToolchainConfiguration(JSON.parse(files["stll-toolchain.json"]));
3206
3382
  disabled = configuration.disabled;
3207
3383
  cloud = configuration.cloud;
3384
+ dynamicSelectors = configuration.dynamicSelectors;
3208
3385
  for (const entry of configuration.engineFloors) engineFloors.push(resolveEngineFloor(entry, files));
3209
3386
  } catch (error) {
3210
3387
  diagnostics.push({
@@ -3244,6 +3421,7 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
3244
3421
  }
3245
3422
  const exercisedFloors = /* @__PURE__ */ new Set();
3246
3423
  const mismatchedFloors = /* @__PURE__ */ new Set();
3424
+ const selectorMatches = /* @__PURE__ */ new Map();
3247
3425
  diagnostics.push(...checkPackageFiles({
3248
3426
  files,
3249
3427
  policy
@@ -3259,6 +3437,8 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
3259
3437
  }
3260
3438
  },
3261
3439
  trackedFiles,
3440
+ dynamicSelectors: dynamicSelectors.filter((entry) => entry.path === file),
3441
+ onDynamicSelector: (entry) => selectorMatches.set(entry, (selectorMatches.get(entry) ?? 0) + 1),
3262
3442
  repository,
3263
3443
  onDelegated,
3264
3444
  engineFloors: engineFloors.filter((entry) => entry.workflow === file),
@@ -3274,6 +3454,12 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
3274
3454
  line: 1,
3275
3455
  message: `engine floor ${entry.workflow}:${entry.job} requires a setup-node exact patch satisfying ${entry.package} engines.node minimum major ${entry.major}`
3276
3456
  });
3457
+ for (const entry of dynamicSelectors) if (selectorMatches.get(entry) !== 1) diagnostics.push({
3458
+ rule: "configuration",
3459
+ path: "stll-toolchain.json",
3460
+ line: 1,
3461
+ message: `dynamic selector ${entry.path}:${entry.at ?? entry.line} must match exactly one unresolved ${entry.kind} selector`
3462
+ });
3277
3463
  diagnostics.push(...checkDependabot({
3278
3464
  files,
3279
3465
  policy: policy.dependabot