@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.
@@ -669,7 +669,7 @@ const generateCloudSetup = ({ policy, cloud, nodeVersion }) => {
669
669
  };
670
670
  //#endregion
671
671
  //#region src/toolchain-container-inputs.ts
672
- const record$6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
672
+ const record$7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
673
673
  /** Filename class recognized by Dependabot's Docker Compose fetcher. */
674
674
  const isComposeDefinitionPath = (file) => /^\.?(?:docker-)?compose(?:-[\w]+)?(?:\.[\w-]+)?\.ya?ml$/i.test(file.split("/").at(-1) ?? "");
675
675
  /** Candidate YAML files are classified by their document shape after reading. */
@@ -716,11 +716,11 @@ const kubernetesPodSpecPaths = {
716
716
  };
717
717
  /** Shared semantic image enumeration for runtime checks and update-root generation. */
718
718
  const containerDocumentImages = (document, file) => {
719
- if (!record$6(document)) return void 0;
719
+ if (!record$7(document)) return void 0;
720
720
  const services = document["services"];
721
- if (isComposeDefinitionPath(file) && record$6(services)) {
721
+ if (isComposeDefinitionPath(file) && record$7(services)) {
722
722
  const images = [];
723
- for (const [name, service] of Object.entries(services)) if (record$6(service) && service["image"] !== void 0) images.push({
723
+ for (const [name, service] of Object.entries(services)) if (record$7(service) && service["image"] !== void 0) images.push({
724
724
  image: service["image"],
725
725
  path: [
726
726
  "services",
@@ -741,7 +741,7 @@ const containerDocumentImages = (document, file) => {
741
741
  visited.add(resource);
742
742
  if (typeof resource["apiVersion"] !== "string" || typeof resource["kind"] !== "string") return;
743
743
  if (resource["kind"] === "List" && Array.isArray(resource["items"])) {
744
- for (const [index, item] of resource["items"].entries()) if (record$6(item)) walkResource(item, [
744
+ for (const [index, item] of resource["items"].entries()) if (record$7(item)) walkResource(item, [
745
745
  ...prefix,
746
746
  "items",
747
747
  index
@@ -751,8 +751,8 @@ const containerDocumentImages = (document, file) => {
751
751
  const specPath = Object.entries(kubernetesPodSpecPaths).find(([kind]) => kind === resource["kind"])?.[1];
752
752
  if (specPath === void 0) return;
753
753
  let spec = resource;
754
- for (const key of specPath) spec = record$6(spec) ? spec[key] : void 0;
755
- if (!record$6(spec)) return;
754
+ for (const key of specPath) spec = record$7(spec) ? spec[key] : void 0;
755
+ if (!record$7(spec)) return;
756
756
  for (const key of [
757
757
  "containers",
758
758
  "initContainers",
@@ -760,7 +760,7 @@ const containerDocumentImages = (document, file) => {
760
760
  ]) {
761
761
  const containers = spec[key];
762
762
  if (!Array.isArray(containers)) continue;
763
- for (const [index, container] of containers.entries()) if (record$6(container) && container["image"] !== void 0) images.push({
763
+ for (const [index, container] of containers.entries()) if (record$7(container) && container["image"] !== void 0) images.push({
764
764
  image: container["image"],
765
765
  path: [
766
766
  ...prefix,
@@ -863,7 +863,7 @@ const workspaceContains = ({ directory, file, patterns, rootMembership }) => {
863
863
  //#endregion
864
864
  //#region src/toolchain-dependabot.ts
865
865
  const dependabotRules = ["dependabot-policy"];
866
- const record$5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
866
+ const record$6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
867
867
  const strings = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
868
868
  const sameStrings = (value, expected) => {
869
869
  if (!strings(value) || value.length !== expected.length) return false;
@@ -885,8 +885,8 @@ const workspaceTable = (parsed, ecosystem) => {
885
885
  case "cargo": return parsed["workspace"];
886
886
  case "uv": {
887
887
  const tool = parsed["tool"];
888
- const uv = record$5(tool) ? tool["uv"] : void 0;
889
- return record$5(uv) ? uv["workspace"] : void 0;
888
+ const uv = record$6(tool) ? tool["uv"] : void 0;
889
+ return record$6(uv) ? uv["workspace"] : void 0;
890
890
  }
891
891
  default: throw new Error("unknown workspace ecosystem", { cause: ecosystem });
892
892
  }
@@ -906,7 +906,7 @@ const workspaceRoots = (files, ecosystem) => {
906
906
  const document = (0, yaml.parseDocument)(text);
907
907
  if (document.errors.length > 0) throw new Error(`${workspaceFile}:1: invalid pnpm workspace YAML`);
908
908
  const parsed = document.toJS({ maxAliasCount: 100 });
909
- if (!record$5(parsed)) throw new Error(`${workspaceFile}:1: pnpm workspace must contain an object`);
909
+ if (!record$6(parsed)) throw new Error(`${workspaceFile}:1: pnpm workspace must contain an object`);
910
910
  const patterns = parsed["packages"] ?? [];
911
911
  if (!strings(patterns)) throw new Error(`${workspaceFile}:1: pnpm packages must be a string array`);
912
912
  workspaces.set(directory, patterns.map((pattern) => pattern.startsWith("!") ? `!${node_path.default.posix.normalize(pattern.slice(1))}` : node_path.default.posix.normalize(pattern)));
@@ -918,13 +918,13 @@ const workspaceRoots = (files, ecosystem) => {
918
918
  }
919
919
  try {
920
920
  const parsed = ecosystem === "npm" ? JSON.parse(files[file] ?? "") : (0, smol_toml.parse)(files[file] ?? "");
921
- if (!record$5(parsed)) continue;
921
+ if (!record$6(parsed)) continue;
922
922
  if (ecosystem === "npm" && typeof parsed["packageManager"] === "string" && parsed["packageManager"].startsWith("pnpm@")) continue;
923
923
  const value = workspaceTable(parsed, ecosystem);
924
- const patterns = record$5(value) ? value[ecosystem === "npm" ? "packages" : "members"] : value;
925
- const members = ecosystem === "cargo" && record$5(value) && patterns === void 0 ? [] : patterns;
924
+ const patterns = record$6(value) ? value[ecosystem === "npm" ? "packages" : "members"] : value;
925
+ const members = ecosystem === "cargo" && record$6(value) && patterns === void 0 ? [] : patterns;
926
926
  if (strings(members)) {
927
- const excluded = ecosystem !== "npm" && record$5(value) && strings(value["exclude"]) ? value["exclude"].map((pattern) => `!${pattern}`) : [];
927
+ const excluded = ecosystem !== "npm" && record$6(value) && strings(value["exclude"]) ? value["exclude"].map((pattern) => `!${pattern}`) : [];
928
928
  workspaces.set(node_path.default.posix.dirname(file), [...members, ...excluded]);
929
929
  }
930
930
  } catch {}
@@ -975,11 +975,11 @@ const workspaceRoots = (files, ecosystem) => {
975
975
  for (const directory of memberDirectories) try {
976
976
  const parsed = (0, smol_toml.parse)(files[node_path.default.posix.join(directory, "Cargo.toml")] ?? "");
977
977
  const workspace = (0, smol_toml.parse)(files[node_path.default.posix.join(root, "Cargo.toml")] ?? "")["workspace"];
978
- const inherited = record$5(workspace) ? workspace["dependencies"] : void 0;
978
+ const inherited = record$6(workspace) ? workspace["dependencies"] : void 0;
979
979
  const dependencyOwners = [parsed];
980
980
  const targets = parsed["target"];
981
- if (record$5(targets)) {
982
- for (const target of Object.values(targets)) if (record$5(target)) dependencyOwners.push(target);
981
+ if (record$6(targets)) {
982
+ for (const target of Object.values(targets)) if (record$6(target)) dependencyOwners.push(target);
983
983
  }
984
984
  for (const owner of dependencyOwners) for (const key of [
985
985
  "dependencies",
@@ -987,12 +987,12 @@ const workspaceRoots = (files, ecosystem) => {
987
987
  "build-dependencies"
988
988
  ]) {
989
989
  const dependencies = owner[key];
990
- if (!record$5(dependencies)) continue;
990
+ if (!record$6(dependencies)) continue;
991
991
  for (const [name, dependency] of Object.entries(dependencies)) {
992
- if (!record$5(dependency)) continue;
992
+ if (!record$6(dependency)) continue;
993
993
  const isInherited = dependency["workspace"] === true;
994
- const resolved = isInherited && record$5(inherited) ? inherited[name] : dependency;
995
- if (!record$5(resolved) || typeof resolved["path"] !== "string") continue;
994
+ const resolved = isInherited && record$6(inherited) ? inherited[name] : dependency;
995
+ if (!record$6(resolved) || typeof resolved["path"] !== "string") continue;
996
996
  if (node_path.default.posix.isAbsolute(resolved["path"]) || /\\|\$|^[A-Za-z]:/.test(resolved["path"])) continue;
997
997
  const target = node_path.default.posix.normalize(node_path.default.posix.join(isInherited ? root : directory, resolved["path"]));
998
998
  const relative = node_path.default.posix.relative(root, target);
@@ -1020,7 +1020,7 @@ const javascriptEcosystem = (directory, files) => {
1020
1020
  if (files[node_path.default.posix.join(root, "bun.lock")] !== void 0) return "bun";
1021
1021
  try {
1022
1022
  const manifest = JSON.parse(files[node_path.default.posix.join(root, "package.json")] ?? "{}");
1023
- if (record$5(manifest) && typeof manifest["packageManager"] === "string" && manifest["packageManager"].startsWith("bun@")) return "bun";
1023
+ if (record$6(manifest) && typeof manifest["packageManager"] === "string" && manifest["packageManager"].startsWith("bun@")) return "bun";
1024
1024
  } catch {}
1025
1025
  return "npm";
1026
1026
  };
@@ -1032,7 +1032,7 @@ const pythonEcosystem = (file, files) => {
1032
1032
  const pyproject = files[node_path.default.posix.join(root, "pyproject.toml")];
1033
1033
  if (pyproject !== void 0) try {
1034
1034
  const tool = (0, smol_toml.parse)(pyproject)["tool"];
1035
- if (record$5(tool) && record$5(tool["uv"])) return "uv";
1035
+ if (record$6(tool) && record$6(tool["uv"])) return "uv";
1036
1036
  } catch {}
1037
1037
  return "pip";
1038
1038
  };
@@ -1103,7 +1103,7 @@ const checkDependabot = ({ files, policy }) => {
1103
1103
  add("cannot resolve Dependabot YAML aliases");
1104
1104
  return diagnostics;
1105
1105
  }
1106
- if (!record$5(parsed) || parsed["version"] !== 2 || !Array.isArray(parsed["updates"])) {
1106
+ if (!record$6(parsed) || parsed["version"] !== 2 || !Array.isArray(parsed["updates"])) {
1107
1107
  add("Dependabot must declare version: 2 and an updates array");
1108
1108
  return diagnostics;
1109
1109
  }
@@ -1116,7 +1116,7 @@ const checkDependabot = ({ files, policy }) => {
1116
1116
  });
1117
1117
  if (legacyDocument.errors.length === 0) legacy = legacyDocument.toJS({ maxAliasCount: 100 });
1118
1118
  } catch {}
1119
- const legacyUpdates = record$5(legacy) && Array.isArray(legacy["updates"]) ? legacy["updates"] : [];
1119
+ const legacyUpdates = record$6(legacy) && Array.isArray(legacy["updates"]) ? legacy["updates"] : [];
1120
1120
  const rootClaims = /* @__PURE__ */ new Map();
1121
1121
  const directoryClaims = /* @__PURE__ */ new Map();
1122
1122
  for (const [index, update] of parsed["updates"].entries()) {
@@ -1130,7 +1130,7 @@ const checkDependabot = ({ files, policy }) => {
1130
1130
  return offset === void 0 ? 1 : lineCounter.linePos(offset).line;
1131
1131
  };
1132
1132
  const report = (key, message) => add(message, lineOf(key));
1133
- if (!record$5(update) || typeof update["package-ecosystem"] !== "string") {
1133
+ if (!record$6(update) || typeof update["package-ecosystem"] !== "string") {
1134
1134
  report("package-ecosystem", "each Dependabot update must declare a package ecosystem");
1135
1135
  continue;
1136
1136
  }
@@ -1164,26 +1164,75 @@ const checkDependabot = ({ files, policy }) => {
1164
1164
  covered.set(ecosystem, matches);
1165
1165
  const schedule = update["schedule"];
1166
1166
  const legacyUpdate = legacyUpdates.at(index);
1167
- const legacySchedule = record$5(legacyUpdate) ? legacyUpdate["schedule"] : void 0;
1168
- 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`);
1169
- 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`);
1167
+ const legacySchedule = record$6(legacyUpdate) ? legacyUpdate["schedule"] : void 0;
1168
+ 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`);
1169
+ 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`);
1170
1170
  const cooldown = update["cooldown"];
1171
- 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}`);
1171
+ 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}`);
1172
1172
  const groups = update["groups"];
1173
- if (!record$5(groups) || Object.keys(groups).length !== Object.keys(policy.groups).length || Object.entries(policy.groups).some(([name, expected]) => {
1173
+ if (!record$6(groups) || Object.keys(groups).length !== Object.keys(policy.groups).length || Object.entries(policy.groups).some(([name, expected]) => {
1174
1174
  const group = groups[name];
1175
- return !record$5(group) || Object.keys(group).length !== 2 || !sameStrings(group["patterns"], expected.patterns) || !sameStrings(group["update-types"], expected.updateTypes);
1175
+ return !record$6(group) || Object.keys(group).length !== 2 || !sameStrings(group["patterns"], expected.patterns) || !sameStrings(group["update-types"], expected.updateTypes);
1176
1176
  })) report("groups", `${ecosystem}: groups must match the shared policy`);
1177
1177
  const ignored = ignoredFor(ecosystem, policy);
1178
1178
  const ignore = update["ignore"];
1179
1179
  if (ignore !== void 0 && (!Array.isArray(ignore) || ignore.length === 0)) report("ignore", `${ecosystem}: omit ignore or provide a nonempty array`);
1180
- 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`);
1181
- 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}`);
1180
+ 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`);
1181
+ 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}`);
1182
1182
  }
1183
1183
  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}`);
1184
1184
  return diagnostics;
1185
1185
  };
1186
1186
  //#endregion
1187
+ //#region src/toolchain-dynamic-selectors.ts
1188
+ const locatorValidators = {
1189
+ image: /^(?:jobs\.[\w-]+\.(?:container|services\.[\w-]+)|services\.[\w.-]+\.image)$/,
1190
+ "bun-source": /^(?:jobs\.[\w-]+|runs)\.steps\.[\w-]+$/
1191
+ };
1192
+ const knownKind = (value) => typeof value === "string" && Object.hasOwn(locatorValidators, value);
1193
+ const record$5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1194
+ /** One reviewed declaration identifies one unresolved selector, never a rule. */
1195
+ const parseDynamicSelectors = (input) => {
1196
+ if (input === void 0) return [];
1197
+ if (!Array.isArray(input)) throw new Error("dynamicSelectors must be an array");
1198
+ const result = [];
1199
+ const keys = /* @__PURE__ */ new Set();
1200
+ for (const entry of input) {
1201
+ if (!record$5(entry) || Object.keys(entry).some((key) => ![
1202
+ "path",
1203
+ "kind",
1204
+ "reason",
1205
+ "at",
1206
+ "line"
1207
+ ].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");
1208
+ const kind = entry["kind"];
1209
+ if (!knownKind(kind)) throw new Error("unknown dynamic selector kind");
1210
+ let selector;
1211
+ if (Object.hasOwn(entry, "line") && !Object.hasOwn(entry, "at") && typeof entry["line"] === "number" && Number.isSafeInteger(entry["line"]) && entry["line"] > 0) selector = {
1212
+ path: entry["path"],
1213
+ kind,
1214
+ reason: entry["reason"],
1215
+ line: entry["line"]
1216
+ };
1217
+ else if (!Object.hasOwn(entry, "line") && typeof entry["at"] === "string" && locatorValidators[kind].test(entry["at"])) selector = {
1218
+ path: entry["path"],
1219
+ kind,
1220
+ reason: entry["reason"],
1221
+ at: entry["at"]
1222
+ };
1223
+ else throw new Error("dynamic selector requires one positive line or a kind-specific job/service/step ID locator");
1224
+ const key = JSON.stringify([
1225
+ selector.path,
1226
+ selector.kind,
1227
+ selector.at ?? selector.line
1228
+ ]);
1229
+ if (keys.has(key)) throw new Error("dynamic selector locations must be unique");
1230
+ keys.add(key);
1231
+ result.push(selector);
1232
+ }
1233
+ return result;
1234
+ };
1235
+ //#endregion
1187
1236
  //#region src/toolchain-engine-floors.ts
1188
1237
  const record$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1189
1238
  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 !== "..");
@@ -1995,7 +2044,7 @@ const pythonRequirementMatches = (requirement, version) => {
1995
2044
  });
1996
2045
  };
1997
2046
  /** Check runtime declarations only in the files that declare or configure them. */
1998
- const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, repository, onDelegated, engineFloors = [], onEngineFloor }) => {
2047
+ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, repository, onDelegated, dynamicSelectors = [], onDynamicSelector, engineFloors = [], onEngineFloor }) => {
1999
2048
  const diagnostics = [];
2000
2049
  const name = node_path.default.posix.basename(file);
2001
2050
  const lines = text.split(/\r?\n/);
@@ -2007,6 +2056,16 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2007
2056
  message
2008
2057
  });
2009
2058
  };
2059
+ let currentLocation;
2060
+ const dynamic = (value) => typeof value === "string" && value.includes("$");
2061
+ const authorizeDynamic = (kind, line, at = currentLocation) => {
2062
+ const matches = dynamicSelectors.filter((entry) => entry.path === file && entry.kind === kind && (entry.at !== void 0 ? entry.at === at : entry.line === line));
2063
+ if (matches.length !== 1) return false;
2064
+ const entry = matches.at(0);
2065
+ if (entry === void 0) return false;
2066
+ onDynamicSelector?.(entry);
2067
+ return true;
2068
+ };
2010
2069
  const lineOf = (key) => Math.max(1, lines.findIndex((line) => line.includes(key)) + 1);
2011
2070
  const pin = ({ rule, value, expected, label, line = 1, selector }) => {
2012
2071
  let matches = value === expected;
@@ -2208,12 +2267,13 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2208
2267
  line: index + 1
2209
2268
  });
2210
2269
  });
2211
- const checkRuntimeImage = ({ image, line, label }) => {
2270
+ const checkRuntimeImage = ({ image, line, label, at }) => {
2212
2271
  const unknownRuntime = () => {
2272
+ if (dynamic(image) && authorizeDynamic("image", line, at)) return;
2213
2273
  add({
2214
2274
  rule: "runtime-docker",
2215
2275
  line,
2216
- message: `cannot determine ${label} runtime; use a static image or a reasoned runtime-docker opt-out`
2276
+ message: `cannot determine ${label} runtime; use a static image or a scoped dynamicSelectors declaration`
2217
2277
  });
2218
2278
  };
2219
2279
  if (typeof image !== "string") {
@@ -2442,6 +2502,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2442
2502
  checkRuntimeImage({
2443
2503
  image: entry.image,
2444
2504
  line,
2505
+ at: entry.path.join("."),
2445
2506
  label: classified?.ecosystem === "docker-compose" ? "Compose image" : "Kubernetes image"
2446
2507
  });
2447
2508
  }
@@ -2476,8 +2537,22 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2476
2537
  let checkoutBindings = [];
2477
2538
  let checkoutDestinations = /* @__PURE__ */ new Map();
2478
2539
  let unknownCheckoutDestination = false;
2540
+ let unreviewableCheckoutSource = false;
2479
2541
  const staticRepositoryPath = (value) => typeof value === "string" && value !== "" && !value.includes("${{") && !value.includes("\\") && !value.includes(":") && !node_path.default.posix.isAbsolute(value) && !value.split("/").includes("..");
2480
2542
  const normalizeCheckoutPath = (prefix) => node_path.default.posix.normalize(prefix).replace(/\/$/, "") || ".";
2543
+ const checkoutProvidersFor = (selector) => {
2544
+ if (selector === void 0) return checkoutBindings;
2545
+ let deepest;
2546
+ for (const entry of checkoutBindings) {
2547
+ if (entry.path !== "." && selector !== entry.path && !selector.startsWith(`${entry.path}/`)) continue;
2548
+ if (deepest === void 0 || entry.path !== "." && (deepest.path === "." || entry.path.length > deepest.path.length)) deepest = entry;
2549
+ }
2550
+ return checkoutBindings.filter((entry) => entry.path === deepest?.path);
2551
+ };
2552
+ const hasAmbiguousCheckoutWriters = (selector, providers) => {
2553
+ if (providers.length > 0) return providers.some((entry) => (checkoutDestinations.get(entry.path) ?? 0) > 1);
2554
+ return [...checkoutDestinations].some(([prefix, count]) => count > 1 && (selector === void 0 || prefix === "." || selector === prefix || selector.startsWith(`${prefix}/`)));
2555
+ };
2481
2556
  const checkReference = ({ value, tool, line }) => {
2482
2557
  const allowed = {
2483
2558
  node: [".node-version", ".nvmrc"],
@@ -2486,8 +2561,28 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2486
2561
  }[tool];
2487
2562
  const rule = tool === "bun" ? "bun-pins" : "runtime-workflow";
2488
2563
  const selector = staticRepositoryPath(value) ? node_path.default.posix.normalize(value) : void 0;
2489
- const binding = selector === void 0 ? void 0 : checkoutBindings.findLast((entry) => entry.path === "." || selector === entry.path || selector.startsWith(`${entry.path}/`));
2564
+ const providers = checkoutProvidersFor(selector);
2565
+ const binding = selector === void 0 ? void 0 : providers.at(-1);
2566
+ const dynamicSource = providers.some((entry) => entry.dynamicSource);
2567
+ const sparseFailure = providers.find((entry) => {
2568
+ if (entry.sparse.mode === "invalid") return true;
2569
+ if (entry.sparse.mode !== "files" && entry.sparse.mode !== "mixed") return false;
2570
+ if (selector === void 0) return true;
2571
+ const selectedFile = entry.path === "." ? selector : node_path.default.posix.relative(entry.path, selector);
2572
+ return !entry.sparse.paths.has(selectedFile);
2573
+ });
2574
+ if (sparseFailure !== void 0) {
2575
+ let missingFile = "the selected version file";
2576
+ if (selector !== void 0) missingFile = sparseFailure.path === "." ? selector : node_path.default.posix.relative(sparseFailure.path, selector);
2577
+ add({
2578
+ rule,
2579
+ line,
2580
+ message: `checkout sparse-checkout must explicitly list ${missingFile} without invalid literal patterns`
2581
+ });
2582
+ return;
2583
+ }
2490
2584
  if (selector === void 0) {
2585
+ if (tool === "bun" && dynamic(value) && !unreviewableCheckoutSource && authorizeDynamic("bun-source", line)) return;
2491
2586
  add({
2492
2587
  rule,
2493
2588
  line,
@@ -2495,7 +2590,8 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2495
2590
  });
2496
2591
  return;
2497
2592
  }
2498
- if (unknownCheckoutDestination || [...checkoutDestinations].some(([prefix, count]) => count > 1 && (prefix === "." || selector === prefix || selector.startsWith(`${prefix}/`)))) {
2593
+ if (unknownCheckoutDestination || hasAmbiguousCheckoutWriters(selector, providers)) {
2594
+ if (tool === "bun" && dynamicSource && !unreviewableCheckoutSource && authorizeDynamic("bun-source", line)) return;
2499
2595
  add({
2500
2596
  rule,
2501
2597
  line,
@@ -2504,6 +2600,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2504
2600
  return;
2505
2601
  }
2506
2602
  if (binding?.source === "untrusted") {
2603
+ if (tool === "bun" && dynamicSource && !unreviewableCheckoutSource && authorizeDynamic("bun-source", line)) return;
2507
2604
  add({
2508
2605
  rule,
2509
2606
  line: binding.line,
@@ -2520,13 +2617,15 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2520
2617
  });
2521
2618
  return;
2522
2619
  }
2523
- if (binding !== void 0 && (binding.sparse.mode === "invalid" || binding.sparse.mode === "files" && !binding.sparse.paths.has(target))) {
2524
- add({
2525
- rule,
2526
- line,
2527
- message: `checkout sparse-checkout must explicitly list ${target} without dynamic, glob, or negation patterns`
2528
- });
2529
- return;
2620
+ if (binding !== void 0 && (binding.sparse.mode === "dynamic" || binding.sparse.mode === "mixed")) {
2621
+ if (tool !== "bun" || unreviewableCheckoutSource || !authorizeDynamic("bun-source", line)) {
2622
+ add({
2623
+ rule,
2624
+ line,
2625
+ message: `checkout sparse-checkout must explicitly list ${target} without dynamic, glob, or negation patterns`
2626
+ });
2627
+ return;
2628
+ }
2530
2629
  }
2531
2630
  if (binding?.source === "delegated") {
2532
2631
  onDelegated?.({
@@ -2648,10 +2747,10 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2648
2747
  return inputs;
2649
2748
  };
2650
2749
  const getInput = (node, key) => inputsOf(node).get(key.toUpperCase());
2651
- const dynamicRefBody = (value) => {
2652
- if (typeof value !== "string" || !value.startsWith("${{")) return void 0;
2750
+ const expressionAt = (value, start) => {
2751
+ if (!value.startsWith("${{", start)) return void 0;
2653
2752
  let quote;
2654
- for (let index = 3; index < value.length; index++) {
2753
+ for (let index = start + 3; index < value.length; index++) {
2655
2754
  const character = value.at(index);
2656
2755
  if (quote !== void 0) {
2657
2756
  if (character === quote) if (quote === "'" && value.at(index + 1) === "'") index++;
@@ -2661,9 +2760,33 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2661
2760
  }
2662
2761
  if (character === "'" || character === "\"") quote = character;
2663
2762
  else if (value.startsWith("${{", index)) return void 0;
2664
- else if (character === "}" && value.at(index + 1) === "}") return index === value.length - 2 ? value.slice(3, index).trim() : void 0;
2763
+ else if (character === "}" && value.at(index + 1) === "}") return {
2764
+ body: value.slice(start + 3, index).trim(),
2765
+ end: index + 2
2766
+ };
2665
2767
  }
2666
2768
  };
2769
+ const dynamicRefBody = (value) => {
2770
+ if (typeof value !== "string") return void 0;
2771
+ const expression = expressionAt(value, 0);
2772
+ return expression?.end === value.length ? expression.body : void 0;
2773
+ };
2774
+ const sparseEntryIsValid = (entry) => {
2775
+ let literalPath = "";
2776
+ let position = 0;
2777
+ while (position < entry.length) {
2778
+ const start = entry.indexOf("${{", position);
2779
+ const literal = entry.slice(position, start < 0 ? entry.length : start);
2780
+ if (/[!$*?[\]\\:]/.test(literal) || literal.split("/").includes("..")) return false;
2781
+ literalPath += literal;
2782
+ if (start < 0) break;
2783
+ const expression = expressionAt(entry, start);
2784
+ if (expression === void 0 || expression.body === "") return false;
2785
+ literalPath += "expression";
2786
+ position = expression.end;
2787
+ }
2788
+ return staticRepositoryPath(literalPath);
2789
+ };
2667
2790
  const checkoutPrefixOf = (node) => {
2668
2791
  const destination = getInput(node, "path");
2669
2792
  if (destination === void 0) return ".";
@@ -2676,11 +2799,19 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2676
2799
  const value = getInput(node, "sparse-checkout");
2677
2800
  const cone = getInput(node, "sparse-checkout-cone-mode");
2678
2801
  if (!(0, yaml.isScalar)(value) || typeof value.value !== "string" || cone !== void 0 && (!(0, yaml.isScalar)(cone) || cone.value !== true && cone.value !== false && cone.value !== "true" && cone.value !== "false")) return { mode: "invalid" };
2679
- const paths = value.value.split(/\r?\n/).map((entry) => entry.trim()).filter((entry) => entry !== "");
2680
- if (paths.length === 0 || paths.some((entry) => !staticRepositoryPath(entry) || entry.startsWith("!") || /[*?[\]]/.test(entry))) return { mode: "invalid" };
2802
+ const paths = value.value.split(/\r?\n/).map((entry) => entry.trim().replace(/^\//, "")).filter((entry) => entry !== "");
2803
+ if (paths.length === 0 || paths.some((entry) => !sparseEntryIsValid(entry))) return { mode: "invalid" };
2804
+ const literalPaths = new Set(paths.filter((entry) => !dynamic(entry)));
2805
+ if (paths.some(dynamic)) {
2806
+ if (literalPaths.size === 0) return { mode: "dynamic" };
2807
+ return {
2808
+ mode: "mixed",
2809
+ paths: literalPaths
2810
+ };
2811
+ }
2681
2812
  return {
2682
2813
  mode: "files",
2683
- paths: new Set(paths)
2814
+ paths: literalPaths
2684
2815
  };
2685
2816
  };
2686
2817
  let currentEngineFloor;
@@ -2710,8 +2841,9 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2710
2841
  }
2711
2842
  if (value.startsWith("./")) {
2712
2843
  const selector = staticRepositoryPath(value) ? node_path.default.posix.normalize(value) : void 0;
2713
- const binding = selector === void 0 ? void 0 : checkoutBindings.findLast((entry) => entry.path === "." || selector === entry.path || selector.startsWith(`${entry.path}/`));
2714
- if (selector === void 0 || unknownCheckoutDestination || binding !== void 0 && binding.source !== "tracked" || [...checkoutDestinations].some(([prefix, count]) => count > 1 && (prefix === "." || selector === prefix || selector.startsWith(`${prefix}/`)))) add({
2844
+ const providers = checkoutProvidersFor(selector);
2845
+ const binding = selector === void 0 ? void 0 : providers.at(-1);
2846
+ if (selector === void 0 || unknownCheckoutDestination || binding !== void 0 && binding.source !== "tracked" || hasAmbiguousCheckoutWriters(selector, providers)) add({
2715
2847
  rule: "action-pins",
2716
2848
  line,
2717
2849
  message: "local actions require an unambiguous checkout of the current source snapshot"
@@ -2767,24 +2899,55 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2767
2899
  const checkoutRepository = getInput(node, "repository");
2768
2900
  const checkoutRef = getInput(node, "ref");
2769
2901
  const prefix = checkoutPrefixOf(node);
2902
+ const clean = getInput(node, "clean");
2903
+ const cleanIsResolved = clean === void 0 || (0, yaml.isScalar)(clean) && !dynamic(clean.value);
2770
2904
  let repo;
2771
2905
  if (checkoutRepository !== void 0) repo = (0, yaml.isScalar)(checkoutRepository) ? checkoutRepository.value : null;
2772
2906
  const self = repo === void 0 || repo === "${{ github.repository }}" || typeof repo === "string" && repository !== void 0 && repo.toLowerCase() === repository.toLowerCase();
2773
2907
  const workflowSource = repo === "${{ job.workflow_repository }}" && (0, yaml.isScalar)(checkoutRef) && checkoutRef.value === "${{ job.workflow_sha }}";
2774
- const currentSource = self && checkoutRef === void 0;
2775
- const refValue = (0, yaml.isScalar)(checkoutRef) ? checkoutRef.value : void 0;
2908
+ const triggers = getNode(document.contents, "on");
2909
+ const callerContext = (() => {
2910
+ if ((0, yaml.isMap)(triggers)) return [...keysOf(triggers)];
2911
+ if ((0, yaml.isSeq)(triggers)) return triggers.items.map((event) => (0, yaml.isScalar)(event) ? event.value : void 0);
2912
+ if ((0, yaml.isScalar)(triggers)) return [triggers.value];
2913
+ return [];
2914
+ })().includes("workflow_call");
2915
+ let refValue = (0, yaml.isScalar)(checkoutRef) ? checkoutRef.value : void 0;
2916
+ if (callerContext && (repo === void 0 || repo === "${{ github.repository }}") && checkoutRef === void 0) refValue = "${{ github.sha }}";
2917
+ const currentRef = dynamicRefBody(refValue);
2918
+ const currentSource = self && !callerContext && (checkoutRef === void 0 || currentRef === "github.sha");
2776
2919
  const dynamicRef = dynamicRefBody(refValue);
2777
- 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);
2920
+ 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");
2921
+ const validSource = cleanIsResolved && ref === approved?.sha && sourceMap({
2922
+ node,
2923
+ key: "if"
2924
+ }) === void 0 && sourceMap({
2925
+ node,
2926
+ key: "continue-on-error"
2927
+ }) === void 0 && (currentSource || workflowSource || delegatedSource);
2928
+ if (!validSource) unreviewableCheckoutSource = true;
2778
2929
  if (!staticRepositoryPath(prefix)) checkoutBindings.push({
2779
2930
  path: ".",
2931
+ dynamicSource: validSource && dynamic(prefix),
2780
2932
  source: "untrusted",
2781
2933
  reason: "checkout destination must be a static repository path",
2782
2934
  line,
2783
- sparse: { mode: "invalid" }
2935
+ sparse: sparseCheckoutOf(node)
2784
2936
  });
2785
2937
  else {
2786
2938
  const destination = normalizeCheckoutPath(prefix);
2787
- const trustedWriter = ref === approved?.sha && sourceMap({
2939
+ checkoutBindings = checkoutBindings.map((entry) => {
2940
+ if (entry.path === destination || destination !== "." && !entry.path.startsWith(`${destination}/`)) return entry;
2941
+ return {
2942
+ path: entry.path,
2943
+ sparse: { mode: "all" },
2944
+ dynamicSource: false,
2945
+ source: "untrusted",
2946
+ reason: "a later ancestor checkout invalidates this descendant destination",
2947
+ line
2948
+ };
2949
+ });
2950
+ const trustedWriter = cleanIsResolved && ref === approved?.sha && sourceMap({
2788
2951
  node,
2789
2952
  key: "if"
2790
2953
  }) === void 0 && sourceMap({
@@ -2793,18 +2956,21 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2793
2956
  }) === void 0 && checkoutDestinations.get(destination) === 1 && !unknownCheckoutDestination;
2794
2957
  if (trustedWriter && delegatedSource && typeof refValue === "string") checkoutBindings.push({
2795
2958
  path: destination,
2959
+ dynamicSource: validSource && delegatedSource,
2796
2960
  sparse: sparseCheckoutOf(node),
2797
2961
  source: "delegated",
2798
2962
  ref: refValue
2799
2963
  });
2800
2964
  else if (trustedWriter && (workflowSource || currentSource)) checkoutBindings.push({
2801
2965
  path: destination,
2966
+ dynamicSource: validSource && delegatedSource,
2802
2967
  sparse: sparseCheckoutOf(node),
2803
2968
  source: "tracked"
2804
2969
  });
2805
2970
  else {
2806
2971
  let reason = "checkout does not select a trusted repository source snapshot";
2807
- if (ref !== approved?.sha) reason = "actions/checkout must use the approved action SHA";
2972
+ if (!cleanIsResolved) reason = "checkout clean must be statically resolved";
2973
+ else if (ref !== approved?.sha) reason = "actions/checkout must use the approved action SHA";
2808
2974
  else if (sourceMap({
2809
2975
  node,
2810
2976
  key: "if"
@@ -2815,6 +2981,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2815
2981
  else if (checkoutDestinations.get(destination) !== 1 || unknownCheckoutDestination) reason = "checkout destination must have exactly one static writer";
2816
2982
  checkoutBindings.push({
2817
2983
  path: destination,
2984
+ dynamicSource: validSource && delegatedSource,
2818
2985
  sparse: sparseCheckoutOf(node),
2819
2986
  source: "untrusted",
2820
2987
  reason,
@@ -2863,21 +3030,23 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2863
3030
  line: reference === void 0 ? line : nodeLine(reference)
2864
3031
  });
2865
3032
  };
2866
- const checkContainer = (node) => {
3033
+ const checkContainer = (node, at) => {
2867
3034
  node = resolveNode(node);
2868
3035
  if (node === void 0) return;
2869
3036
  const image = (0, yaml.isMap)(node) ? getNode(node, "image") : node;
2870
3037
  checkRuntimeImage({
2871
3038
  image: (0, yaml.isScalar)(image) ? image.value : void 0,
2872
3039
  line: nodeLine(image ?? node),
2873
- label: "container image"
3040
+ label: "container image",
3041
+ at
2874
3042
  });
2875
3043
  };
2876
- const checkSteps = (node, floor) => {
3044
+ const checkSteps = (node, locationPrefix, floor) => {
2877
3045
  currentEngineFloor = void 0;
2878
3046
  checkoutBindings = [];
2879
3047
  checkoutDestinations = /* @__PURE__ */ new Map();
2880
3048
  unknownCheckoutDestination = false;
3049
+ unreviewableCheckoutSource = false;
2881
3050
  node = resolveNode(node);
2882
3051
  if (!(0, yaml.isSeq)(node)) return;
2883
3052
  for (const step of node.items) {
@@ -2893,9 +3062,14 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2893
3062
  }
2894
3063
  currentEngineFloor = floor;
2895
3064
  try {
2896
- for (const step of node.items) checkAction(step);
3065
+ for (const step of node.items) {
3066
+ const id = getNode(step, "id");
3067
+ currentLocation = (0, yaml.isScalar)(id) && typeof id.value === "string" ? `${locationPrefix}.steps.${id.value}` : void 0;
3068
+ checkAction(step);
3069
+ }
2897
3070
  } finally {
2898
3071
  currentEngineFloor = void 0;
3072
+ currentLocation = void 0;
2899
3073
  }
2900
3074
  };
2901
3075
  try {
@@ -2909,15 +3083,15 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2909
3083
  currentEngineFloor = void 0;
2910
3084
  const job = getNode(jobs, key);
2911
3085
  checkAction(job);
2912
- checkContainer(getNode(job, "container"));
3086
+ checkContainer(getNode(job, "container"), `jobs.${key}.container`);
2913
3087
  const services = getNode(job, "services");
2914
- if ((0, yaml.isMap)(services)) for (const service of keysOf(services)) checkContainer(getNode(services, service));
2915
- checkSteps(getNode(job, "steps"), engineFloors.find((entry) => entry.job === key));
3088
+ if ((0, yaml.isMap)(services)) for (const service of keysOf(services)) checkContainer(getNode(services, service), `jobs.${key}.services.${service}`);
3089
+ checkSteps(getNode(job, "steps"), `jobs.${key}`, engineFloors.find((entry) => entry.job === key));
2916
3090
  }
2917
3091
  } else {
2918
3092
  const runs = getNode(document.contents, "runs");
2919
3093
  const using = getNode(runs, "using");
2920
- if ((0, yaml.isScalar)(using) && using.value === "composite") checkSteps(getNode(runs, "steps"));
3094
+ if ((0, yaml.isScalar)(using) && using.value === "composite") checkSteps(getNode(runs, "steps"), "runs");
2921
3095
  if ((0, yaml.isScalar)(using) && using.value === "docker") {
2922
3096
  const image = getNode(runs, "image");
2923
3097
  const value = (0, yaml.isScalar)(image) ? image.value : void 0;
@@ -3146,7 +3320,7 @@ const record = (value) => typeof value === "object" && value !== null && !Array.
3146
3320
  const array = (value) => Array.isArray(value);
3147
3321
  /** Repository-owned declarations are explicit; malformed decisions fail closed. */
3148
3322
  const parseToolchainConfiguration = (input) => {
3149
- 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");
3323
+ 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");
3150
3324
  const disabled = /* @__PURE__ */ new Set();
3151
3325
  const optOuts = input["optOuts"] ?? [];
3152
3326
  if (!array(optOuts)) throw new Error("optOuts must be an array");
@@ -3159,7 +3333,8 @@ const parseToolchainConfiguration = (input) => {
3159
3333
  return {
3160
3334
  disabled,
3161
3335
  cloud: parseCloudSetup(input["cloud"]),
3162
- engineFloors: parseEngineFloors(input["engineFloors"])
3336
+ engineFloors: parseEngineFloors(input["engineFloors"]),
3337
+ dynamicSelectors: parseDynamicSelectors(input["dynamicSelectors"])
3163
3338
  };
3164
3339
  };
3165
3340
  /** Read one tracked configuration snapshot for guards and policy generation. */
@@ -3225,10 +3400,12 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
3225
3400
  let disabled = /* @__PURE__ */ new Set();
3226
3401
  let cloud;
3227
3402
  const engineFloors = [];
3403
+ let dynamicSelectors = [];
3228
3404
  if (files["stll-toolchain.json"] !== void 0) try {
3229
3405
  const configuration = parseToolchainConfiguration(JSON.parse(files["stll-toolchain.json"]));
3230
3406
  disabled = configuration.disabled;
3231
3407
  cloud = configuration.cloud;
3408
+ dynamicSelectors = configuration.dynamicSelectors;
3232
3409
  for (const entry of configuration.engineFloors) engineFloors.push(resolveEngineFloor(entry, files));
3233
3410
  } catch (error) {
3234
3411
  diagnostics.push({
@@ -3268,6 +3445,7 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
3268
3445
  }
3269
3446
  const exercisedFloors = /* @__PURE__ */ new Set();
3270
3447
  const mismatchedFloors = /* @__PURE__ */ new Set();
3448
+ const selectorMatches = /* @__PURE__ */ new Map();
3271
3449
  diagnostics.push(...checkPackageFiles({
3272
3450
  files,
3273
3451
  policy
@@ -3283,6 +3461,8 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
3283
3461
  }
3284
3462
  },
3285
3463
  trackedFiles,
3464
+ dynamicSelectors: dynamicSelectors.filter((entry) => entry.path === file),
3465
+ onDynamicSelector: (entry) => selectorMatches.set(entry, (selectorMatches.get(entry) ?? 0) + 1),
3286
3466
  repository,
3287
3467
  onDelegated,
3288
3468
  engineFloors: engineFloors.filter((entry) => entry.workflow === file),
@@ -3298,6 +3478,12 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
3298
3478
  line: 1,
3299
3479
  message: `engine floor ${entry.workflow}:${entry.job} requires a setup-node exact patch satisfying ${entry.package} engines.node minimum major ${entry.major}`
3300
3480
  });
3481
+ for (const entry of dynamicSelectors) if (selectorMatches.get(entry) !== 1) diagnostics.push({
3482
+ rule: "configuration",
3483
+ path: "stll-toolchain.json",
3484
+ line: 1,
3485
+ message: `dynamic selector ${entry.path}:${entry.at ?? entry.line} must match exactly one unresolved ${entry.kind} selector`
3486
+ });
3301
3487
  diagnostics.push(...checkDependabot({
3302
3488
  files,
3303
3489
  policy: policy.dependabot