@stll/oxlint-config 0.12.0 → 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.
@@ -291,19 +291,28 @@ const parseCloudSetup = (input) => {
291
291
  if (input === void 0) return void 0;
292
292
  if (typeof input !== "object" || input === null || Array.isArray(input)) throw new Error("cloud must be an object");
293
293
  for (const key of Object.keys(input)) if (key !== "services" && key !== "install" && key !== "envFile") throw new Error(`unsupported cloud key: ${key}`);
294
- if (!("services" in input) || !Array.isArray(input.services)) throw new Error("cloud.services must be an explicit array");
294
+ const selected = "services" in input ? input.services : [];
295
+ if (!Array.isArray(selected)) throw new Error("cloud.services must be an array when provided");
295
296
  if (!("install" in input) || input.install !== "bun install --frozen-lockfile") throw new Error(`cloud.install must be exactly ${cloudInstallCommand}`);
297
+ const seen = /* @__PURE__ */ new Set();
298
+ const services = Array.from(selected, (service) => {
299
+ if (!cloudService(service)) throw new Error("cloud.services contains an unsupported service");
300
+ if (seen.has(service)) throw new Error(`cloud.services contains duplicate service: ${service}`);
301
+ seen.add(service);
302
+ return service;
303
+ });
304
+ if (services.length === 0) {
305
+ if ("envFile" in input) throw new Error("cloud.envFile is not allowed without services");
306
+ return {
307
+ services,
308
+ install: cloudInstallCommand
309
+ };
310
+ }
296
311
  if (!("envFile" in input) || typeof input.envFile !== "string") throw new Error("cloud.envFile must be a canonical repository-relative path");
297
312
  const envFile = input.envFile;
298
313
  if (envFile.startsWith("/") || /^[A-Za-z]:/.test(envFile) || envFile.includes("\\") || /\p{Cc}/u.test(envFile) || envFile.split("/").some((part) => part === "" || part === "." || part === "..")) throw new Error("cloud.envFile must be a canonical repository-relative path");
299
- const seen = /* @__PURE__ */ new Set();
300
314
  return {
301
- services: Array.from(input.services, (service) => {
302
- if (!cloudService(service)) throw new Error("cloud.services contains an unsupported service");
303
- if (seen.has(service)) throw new Error(`cloud.services contains duplicate service: ${service}`);
304
- seen.add(service);
305
- return service;
306
- }),
315
+ services,
307
316
  install: cloudInstallCommand,
308
317
  envFile
309
318
  };
@@ -515,8 +524,7 @@ NODE_DIR="/opt/stll-cloud/node/$NODE_VERSION"
515
524
  BUN_DIR="/opt/stll-cloud/bun/$BUN_VERSION"
516
525
  export PATH="$NODE_DIR/bin:$BUN_DIR:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
517
526
  BUN_CACHE="/var/cache/stll-cloud/bun/$OUTPUT_UID"
518
- ENV_FILE=@ENV_FILE@
519
- ENV_MARKER='@ENV_MARKER@'
527
+ @ENV_ASSIGNMENTS@
520
528
  [[ -f "$REPO_ROOT/.node-version" && ! -L "$REPO_ROOT/.node-version" ]] || fail 'A regular root .node-version is required'
521
529
  [[ "$(awk 'NF { if (++count > 1 || NF != 1) exit 1; value=$1 } END { if (count != 1) exit 1; print value }' "$REPO_ROOT/.node-version")" == "$NODE_VERSION" ]] || fail 'Node selector changed; regenerate cloud-setup.sh'
522
530
  cd -- "$REPO_ROOT"
@@ -585,6 +593,21 @@ dependency_install() {
585
593
  @SERVICE_INSTALL@
586
594
  @SERVICE_START@
587
595
 
596
+ @ENV_FUNCTIONS@
597
+
598
+ if [[ "$MODE" == install ]]; then
599
+ install_runtimes
600
+ install_services
601
+ dependency_install
602
+ exit 0
603
+ fi
604
+ runtime_ready
605
+ @ENV_VALIDATE@
606
+ prepare_state
607
+ start_services
608
+ @ENV_WRITE@
609
+ `;
610
+ const environmentFunctions = String.raw`
588
611
  validate_environment() {
589
612
  local part target="$REPO_ROOT" env_path="$REPO_ROOT/$ENV_FILE" first_line temporary
590
613
  while IFS= read -r part; do
@@ -611,20 +634,6 @@ write_environment() {
611
634
  chown "$OUTPUT_UID:$OUTPUT_GID" "$temporary"
612
635
  mv -f -- "$temporary" "$env_path"
613
636
  }
614
-
615
- if [[ "$MODE" == install ]]; then
616
- install_runtimes
617
- install_services
618
- dependency_install
619
- exit 0
620
- fi
621
- runtime_ready
622
- DATABASE_URL=''
623
- REDIS_URL=''
624
- validate_environment
625
- prepare_state
626
- start_services
627
- write_environment
628
637
  `;
629
638
  const shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
630
639
  const generateCloudSetup = ({ policy, cloud, nodeVersion }) => {
@@ -636,8 +645,10 @@ const generateCloudSetup = ({ policy, cloud, nodeVersion }) => {
636
645
  const replacements = /* @__PURE__ */ new Map([
637
646
  ["@BUN@", policy.bun],
638
647
  ["@NODE@", exactNode],
639
- ["@ENV_FILE@", shellQuote(selected.envFile)],
640
- ["@ENV_MARKER@", cloudEnvironmentMarker],
648
+ ["@ENV_ASSIGNMENTS@", selected.envFile === void 0 ? "" : `ENV_FILE=${shellQuote(selected.envFile)}\nENV_MARKER=${shellQuote(cloudEnvironmentMarker)}`],
649
+ ["@ENV_FUNCTIONS@", selected.envFile === void 0 ? "" : environmentFunctions],
650
+ ["@ENV_VALIDATE@", selected.envFile === void 0 ? "" : "DATABASE_URL=''\nREDIS_URL=''\nvalidate_environment"],
651
+ ["@ENV_WRITE@", selected.envFile === void 0 ? "" : "write_environment"],
641
652
  ["@SERVICE_INSTALL@", renderCloudServiceInstall({
642
653
  services,
643
654
  postgres: policy.postgres,
@@ -658,7 +669,7 @@ const generateCloudSetup = ({ policy, cloud, nodeVersion }) => {
658
669
  };
659
670
  //#endregion
660
671
  //#region src/toolchain-container-inputs.ts
661
- 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);
662
673
  /** Filename class recognized by Dependabot's Docker Compose fetcher. */
663
674
  const isComposeDefinitionPath = (file) => /^\.?(?:docker-)?compose(?:-[\w]+)?(?:\.[\w-]+)?\.ya?ml$/i.test(file.split("/").at(-1) ?? "");
664
675
  /** Candidate YAML files are classified by their document shape after reading. */
@@ -705,11 +716,11 @@ const kubernetesPodSpecPaths = {
705
716
  };
706
717
  /** Shared semantic image enumeration for runtime checks and update-root generation. */
707
718
  const containerDocumentImages = (document, file) => {
708
- if (!record$6(document)) return void 0;
719
+ if (!record$7(document)) return void 0;
709
720
  const services = document["services"];
710
- if (isComposeDefinitionPath(file) && record$6(services)) {
721
+ if (isComposeDefinitionPath(file) && record$7(services)) {
711
722
  const images = [];
712
- 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({
713
724
  image: service["image"],
714
725
  path: [
715
726
  "services",
@@ -730,7 +741,7 @@ const containerDocumentImages = (document, file) => {
730
741
  visited.add(resource);
731
742
  if (typeof resource["apiVersion"] !== "string" || typeof resource["kind"] !== "string") return;
732
743
  if (resource["kind"] === "List" && Array.isArray(resource["items"])) {
733
- 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, [
734
745
  ...prefix,
735
746
  "items",
736
747
  index
@@ -740,8 +751,8 @@ const containerDocumentImages = (document, file) => {
740
751
  const specPath = Object.entries(kubernetesPodSpecPaths).find(([kind]) => kind === resource["kind"])?.[1];
741
752
  if (specPath === void 0) return;
742
753
  let spec = resource;
743
- for (const key of specPath) spec = record$6(spec) ? spec[key] : void 0;
744
- 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;
745
756
  for (const key of [
746
757
  "containers",
747
758
  "initContainers",
@@ -749,7 +760,7 @@ const containerDocumentImages = (document, file) => {
749
760
  ]) {
750
761
  const containers = spec[key];
751
762
  if (!Array.isArray(containers)) continue;
752
- 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({
753
764
  image: container["image"],
754
765
  path: [
755
766
  ...prefix,
@@ -852,7 +863,7 @@ const workspaceContains = ({ directory, file, patterns, rootMembership }) => {
852
863
  //#endregion
853
864
  //#region src/toolchain-dependabot.ts
854
865
  const dependabotRules = ["dependabot-policy"];
855
- 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);
856
867
  const strings = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
857
868
  const sameStrings = (value, expected) => {
858
869
  if (!strings(value) || value.length !== expected.length) return false;
@@ -874,8 +885,8 @@ const workspaceTable = (parsed, ecosystem) => {
874
885
  case "cargo": return parsed["workspace"];
875
886
  case "uv": {
876
887
  const tool = parsed["tool"];
877
- const uv = record$5(tool) ? tool["uv"] : void 0;
878
- 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;
879
890
  }
880
891
  default: throw new Error("unknown workspace ecosystem", { cause: ecosystem });
881
892
  }
@@ -895,7 +906,7 @@ const workspaceRoots = (files, ecosystem) => {
895
906
  const document = (0, yaml.parseDocument)(text);
896
907
  if (document.errors.length > 0) throw new Error(`${workspaceFile}:1: invalid pnpm workspace YAML`);
897
908
  const parsed = document.toJS({ maxAliasCount: 100 });
898
- 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`);
899
910
  const patterns = parsed["packages"] ?? [];
900
911
  if (!strings(patterns)) throw new Error(`${workspaceFile}:1: pnpm packages must be a string array`);
901
912
  workspaces.set(directory, patterns.map((pattern) => pattern.startsWith("!") ? `!${node_path.default.posix.normalize(pattern.slice(1))}` : node_path.default.posix.normalize(pattern)));
@@ -907,13 +918,13 @@ const workspaceRoots = (files, ecosystem) => {
907
918
  }
908
919
  try {
909
920
  const parsed = ecosystem === "npm" ? JSON.parse(files[file] ?? "") : (0, smol_toml.parse)(files[file] ?? "");
910
- if (!record$5(parsed)) continue;
921
+ if (!record$6(parsed)) continue;
911
922
  if (ecosystem === "npm" && typeof parsed["packageManager"] === "string" && parsed["packageManager"].startsWith("pnpm@")) continue;
912
923
  const value = workspaceTable(parsed, ecosystem);
913
- const patterns = record$5(value) ? value[ecosystem === "npm" ? "packages" : "members"] : value;
914
- 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;
915
926
  if (strings(members)) {
916
- 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}`) : [];
917
928
  workspaces.set(node_path.default.posix.dirname(file), [...members, ...excluded]);
918
929
  }
919
930
  } catch {}
@@ -964,11 +975,11 @@ const workspaceRoots = (files, ecosystem) => {
964
975
  for (const directory of memberDirectories) try {
965
976
  const parsed = (0, smol_toml.parse)(files[node_path.default.posix.join(directory, "Cargo.toml")] ?? "");
966
977
  const workspace = (0, smol_toml.parse)(files[node_path.default.posix.join(root, "Cargo.toml")] ?? "")["workspace"];
967
- const inherited = record$5(workspace) ? workspace["dependencies"] : void 0;
978
+ const inherited = record$6(workspace) ? workspace["dependencies"] : void 0;
968
979
  const dependencyOwners = [parsed];
969
980
  const targets = parsed["target"];
970
- if (record$5(targets)) {
971
- 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);
972
983
  }
973
984
  for (const owner of dependencyOwners) for (const key of [
974
985
  "dependencies",
@@ -976,12 +987,12 @@ const workspaceRoots = (files, ecosystem) => {
976
987
  "build-dependencies"
977
988
  ]) {
978
989
  const dependencies = owner[key];
979
- if (!record$5(dependencies)) continue;
990
+ if (!record$6(dependencies)) continue;
980
991
  for (const [name, dependency] of Object.entries(dependencies)) {
981
- if (!record$5(dependency)) continue;
992
+ if (!record$6(dependency)) continue;
982
993
  const isInherited = dependency["workspace"] === true;
983
- const resolved = isInherited && record$5(inherited) ? inherited[name] : dependency;
984
- 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;
985
996
  if (node_path.default.posix.isAbsolute(resolved["path"]) || /\\|\$|^[A-Za-z]:/.test(resolved["path"])) continue;
986
997
  const target = node_path.default.posix.normalize(node_path.default.posix.join(isInherited ? root : directory, resolved["path"]));
987
998
  const relative = node_path.default.posix.relative(root, target);
@@ -1009,7 +1020,7 @@ const javascriptEcosystem = (directory, files) => {
1009
1020
  if (files[node_path.default.posix.join(root, "bun.lock")] !== void 0) return "bun";
1010
1021
  try {
1011
1022
  const manifest = JSON.parse(files[node_path.default.posix.join(root, "package.json")] ?? "{}");
1012
- 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";
1013
1024
  } catch {}
1014
1025
  return "npm";
1015
1026
  };
@@ -1021,7 +1032,7 @@ const pythonEcosystem = (file, files) => {
1021
1032
  const pyproject = files[node_path.default.posix.join(root, "pyproject.toml")];
1022
1033
  if (pyproject !== void 0) try {
1023
1034
  const tool = (0, smol_toml.parse)(pyproject)["tool"];
1024
- if (record$5(tool) && record$5(tool["uv"])) return "uv";
1035
+ if (record$6(tool) && record$6(tool["uv"])) return "uv";
1025
1036
  } catch {}
1026
1037
  return "pip";
1027
1038
  };
@@ -1092,7 +1103,7 @@ const checkDependabot = ({ files, policy }) => {
1092
1103
  add("cannot resolve Dependabot YAML aliases");
1093
1104
  return diagnostics;
1094
1105
  }
1095
- if (!record$5(parsed) || parsed["version"] !== 2 || !Array.isArray(parsed["updates"])) {
1106
+ if (!record$6(parsed) || parsed["version"] !== 2 || !Array.isArray(parsed["updates"])) {
1096
1107
  add("Dependabot must declare version: 2 and an updates array");
1097
1108
  return diagnostics;
1098
1109
  }
@@ -1105,7 +1116,7 @@ const checkDependabot = ({ files, policy }) => {
1105
1116
  });
1106
1117
  if (legacyDocument.errors.length === 0) legacy = legacyDocument.toJS({ maxAliasCount: 100 });
1107
1118
  } catch {}
1108
- const legacyUpdates = record$5(legacy) && Array.isArray(legacy["updates"]) ? legacy["updates"] : [];
1119
+ const legacyUpdates = record$6(legacy) && Array.isArray(legacy["updates"]) ? legacy["updates"] : [];
1109
1120
  const rootClaims = /* @__PURE__ */ new Map();
1110
1121
  const directoryClaims = /* @__PURE__ */ new Map();
1111
1122
  for (const [index, update] of parsed["updates"].entries()) {
@@ -1119,7 +1130,7 @@ const checkDependabot = ({ files, policy }) => {
1119
1130
  return offset === void 0 ? 1 : lineCounter.linePos(offset).line;
1120
1131
  };
1121
1132
  const report = (key, message) => add(message, lineOf(key));
1122
- if (!record$5(update) || typeof update["package-ecosystem"] !== "string") {
1133
+ if (!record$6(update) || typeof update["package-ecosystem"] !== "string") {
1123
1134
  report("package-ecosystem", "each Dependabot update must declare a package ecosystem");
1124
1135
  continue;
1125
1136
  }
@@ -1153,26 +1164,75 @@ const checkDependabot = ({ files, policy }) => {
1153
1164
  covered.set(ecosystem, matches);
1154
1165
  const schedule = update["schedule"];
1155
1166
  const legacyUpdate = legacyUpdates.at(index);
1156
- const legacySchedule = record$5(legacyUpdate) ? legacyUpdate["schedule"] : void 0;
1157
- 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`);
1158
- 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`);
1159
1170
  const cooldown = update["cooldown"];
1160
- 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}`);
1161
1172
  const groups = update["groups"];
1162
- 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]) => {
1163
1174
  const group = groups[name];
1164
- 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);
1165
1176
  })) report("groups", `${ecosystem}: groups must match the shared policy`);
1166
1177
  const ignored = ignoredFor(ecosystem, policy);
1167
1178
  const ignore = update["ignore"];
1168
1179
  if (ignore !== void 0 && (!Array.isArray(ignore) || ignore.length === 0)) report("ignore", `${ecosystem}: omit ignore or provide a nonempty array`);
1169
- 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`);
1170
- 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}`);
1171
1182
  }
1172
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}`);
1173
1184
  return diagnostics;
1174
1185
  };
1175
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
1176
1236
  //#region src/toolchain-engine-floors.ts
1177
1237
  const record$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1178
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 !== "..");
@@ -1984,7 +2044,7 @@ const pythonRequirementMatches = (requirement, version) => {
1984
2044
  });
1985
2045
  };
1986
2046
  /** Check runtime declarations only in the files that declare or configure them. */
1987
- 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 }) => {
1988
2048
  const diagnostics = [];
1989
2049
  const name = node_path.default.posix.basename(file);
1990
2050
  const lines = text.split(/\r?\n/);
@@ -1996,6 +2056,16 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
1996
2056
  message
1997
2057
  });
1998
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
+ };
1999
2069
  const lineOf = (key) => Math.max(1, lines.findIndex((line) => line.includes(key)) + 1);
2000
2070
  const pin = ({ rule, value, expected, label, line = 1, selector }) => {
2001
2071
  let matches = value === expected;
@@ -2197,32 +2267,73 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2197
2267
  line: index + 1
2198
2268
  });
2199
2269
  });
2200
- const checkRuntimeImage = ({ image, line, label }) => {
2201
- if (typeof image !== "string" || image.includes("$")) {
2270
+ const checkRuntimeImage = ({ image, line, label, at }) => {
2271
+ const unknownRuntime = () => {
2272
+ if (dynamic(image) && authorizeDynamic("image", line, at)) return;
2202
2273
  add({
2203
2274
  rule: "runtime-docker",
2204
2275
  line,
2205
- 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`
2206
2277
  });
2278
+ };
2279
+ if (typeof image !== "string") {
2280
+ unknownRuntime();
2281
+ return;
2282
+ }
2283
+ const canonical = canonicalDockerRuntime(image);
2284
+ const structuralImage = canonical.replace(/\$\{[^}]*\}|\$[A-Za-z_][A-Za-z_\d]*/g, (variable) => "x".repeat(variable.length));
2285
+ const digestSeparator = structuralImage.indexOf("@");
2286
+ const reference = digestSeparator < 0 ? canonical : canonical.slice(0, digestSeparator);
2287
+ const structuralReference = digestSeparator < 0 ? structuralImage : structuralImage.slice(0, digestSeparator);
2288
+ const managedFamily = /^(node|python|oven\/bun)(?=:|$)/i.exec(structuralReference)?.[1];
2289
+ const tagSeparator = managedFamily === void 0 ? structuralReference.lastIndexOf(":") : structuralReference.indexOf(":", managedFamily.length);
2290
+ const hasTag = managedFamily === void 0 ? tagSeparator > structuralReference.lastIndexOf("/") : tagSeparator !== -1;
2291
+ const imageRepository = hasTag ? reference.slice(0, tagSeparator) : reference;
2292
+ if (imageRepository.length === 0 || imageRepository.includes("$")) {
2293
+ unknownRuntime();
2294
+ return;
2295
+ }
2296
+ if (!/^(node|python|oven\/bun)$/i.test(imageRepository)) return;
2297
+ const tool = imageRepository.toLowerCase();
2298
+ if (image.includes("$")) {
2299
+ unknownRuntime();
2207
2300
  return;
2208
2301
  }
2209
- const runtime = /^(node|python|oven\/bun)(?::([^@]+))?(?:@.*)?$/i.exec(canonicalDockerRuntime(image));
2210
- if (runtime === null) return;
2211
- const tool = runtime[1]?.toLowerCase();
2212
- if (image.includes("@")) {
2302
+ const digest = digestSeparator < 0 ? void 0 : canonical.slice(digestSeparator + 1);
2303
+ if (digest !== void 0 && !/^sha256:[a-fA-F\d]{64}$/.test(digest)) {
2213
2304
  add({
2214
2305
  rule: tool === "oven/bun" ? "bun-pins" : "runtime-docker",
2215
2306
  line,
2216
- message: `${label} ${tool} digest requires an approved digest policy; use a tag-only runtime image`
2307
+ message: `${label} ${tool} digest must be sha256 followed by 64 hexadecimal characters`
2217
2308
  });
2218
2309
  return;
2219
2310
  }
2220
- const tag = runtime[2];
2311
+ const tag = hasTag ? reference.slice(tagSeparator + 1) : void 0;
2221
2312
  const variantSuffix = tool === "oven/bun" ? /-(?:alpine|slim|debian)$/ : /-(?:alpine(?:\d+(?:\.\d+)*)?|(?:bookworm|bullseye|trixie|buster)(?:-slim)?|slim)$/;
2222
2313
  const version = tag?.replace(variantSuffix, "");
2223
2314
  let expected = policy.bun;
2224
2315
  if (tool === "node") expected = policy.node;
2225
2316
  else if (tool === "python") expected = policy.python;
2317
+ if (digest !== void 0) {
2318
+ if (version === void 0 || !/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/.test(version)) {
2319
+ add({
2320
+ rule: tool === "oven/bun" ? "bun-pins" : "runtime-docker",
2321
+ line,
2322
+ message: `${label} ${tool} digest requires an exact stable patch tag matching ${expected}`
2323
+ });
2324
+ return;
2325
+ }
2326
+ if (tool === "node" && trackedFiles.has(".node-version")) {
2327
+ if (readFile(".node-version")?.trim() !== version) {
2328
+ add({
2329
+ rule: "runtime-docker",
2330
+ line,
2331
+ message: `${label} node digest tag must match tracked .node-version, found ${version}`
2332
+ });
2333
+ return;
2334
+ }
2335
+ }
2336
+ }
2226
2337
  pin({
2227
2338
  rule: tool === "oven/bun" ? "bun-pins" : "runtime-docker",
2228
2339
  value: version,
@@ -2335,14 +2446,6 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2335
2446
  if (from === null || from[1] === void 0) return;
2336
2447
  argumentScope = "stage";
2337
2448
  const image = expand(from[1]);
2338
- if (image.includes("$")) {
2339
- add({
2340
- rule: "runtime-docker",
2341
- line,
2342
- message: "cannot determine FROM runtime; declare an image ARG default or a reasoned runtime-docker opt-out"
2343
- });
2344
- return;
2345
- }
2346
2449
  const stage = from[2]?.toLowerCase();
2347
2450
  const previousStage = stages.has(image.toLowerCase());
2348
2451
  if (stage !== void 0) stages.add(stage);
@@ -2399,6 +2502,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2399
2502
  checkRuntimeImage({
2400
2503
  image: entry.image,
2401
2504
  line,
2505
+ at: entry.path.join("."),
2402
2506
  label: classified?.ecosystem === "docker-compose" ? "Compose image" : "Kubernetes image"
2403
2507
  });
2404
2508
  }
@@ -2433,8 +2537,22 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2433
2537
  let checkoutBindings = [];
2434
2538
  let checkoutDestinations = /* @__PURE__ */ new Map();
2435
2539
  let unknownCheckoutDestination = false;
2540
+ let unreviewableCheckoutSource = false;
2436
2541
  const staticRepositoryPath = (value) => typeof value === "string" && value !== "" && !value.includes("${{") && !value.includes("\\") && !value.includes(":") && !node_path.default.posix.isAbsolute(value) && !value.split("/").includes("..");
2437
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
+ };
2438
2556
  const checkReference = ({ value, tool, line }) => {
2439
2557
  const allowed = {
2440
2558
  node: [".node-version", ".nvmrc"],
@@ -2443,8 +2561,28 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2443
2561
  }[tool];
2444
2562
  const rule = tool === "bun" ? "bun-pins" : "runtime-workflow";
2445
2563
  const selector = staticRepositoryPath(value) ? node_path.default.posix.normalize(value) : void 0;
2446
- 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
+ }
2447
2584
  if (selector === void 0) {
2585
+ if (tool === "bun" && dynamic(value) && !unreviewableCheckoutSource && authorizeDynamic("bun-source", line)) return;
2448
2586
  add({
2449
2587
  rule,
2450
2588
  line,
@@ -2452,7 +2590,8 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2452
2590
  });
2453
2591
  return;
2454
2592
  }
2455
- 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;
2456
2595
  add({
2457
2596
  rule,
2458
2597
  line,
@@ -2461,6 +2600,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2461
2600
  return;
2462
2601
  }
2463
2602
  if (binding?.source === "untrusted") {
2603
+ if (tool === "bun" && dynamicSource && !unreviewableCheckoutSource && authorizeDynamic("bun-source", line)) return;
2464
2604
  add({
2465
2605
  rule,
2466
2606
  line: binding.line,
@@ -2477,13 +2617,15 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2477
2617
  });
2478
2618
  return;
2479
2619
  }
2480
- if (binding !== void 0 && (binding.sparse.mode === "invalid" || binding.sparse.mode === "files" && !binding.sparse.paths.has(target))) {
2481
- add({
2482
- rule,
2483
- line,
2484
- message: `checkout sparse-checkout must explicitly list ${target} without dynamic, glob, or negation patterns`
2485
- });
2486
- 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
+ }
2487
2629
  }
2488
2630
  if (binding?.source === "delegated") {
2489
2631
  onDelegated?.({
@@ -2605,10 +2747,10 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2605
2747
  return inputs;
2606
2748
  };
2607
2749
  const getInput = (node, key) => inputsOf(node).get(key.toUpperCase());
2608
- const dynamicRefBody = (value) => {
2609
- if (typeof value !== "string" || !value.startsWith("${{")) return void 0;
2750
+ const expressionAt = (value, start) => {
2751
+ if (!value.startsWith("${{", start)) return void 0;
2610
2752
  let quote;
2611
- for (let index = 3; index < value.length; index++) {
2753
+ for (let index = start + 3; index < value.length; index++) {
2612
2754
  const character = value.at(index);
2613
2755
  if (quote !== void 0) {
2614
2756
  if (character === quote) if (quote === "'" && value.at(index + 1) === "'") index++;
@@ -2618,9 +2760,33 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2618
2760
  }
2619
2761
  if (character === "'" || character === "\"") quote = character;
2620
2762
  else if (value.startsWith("${{", index)) return void 0;
2621
- 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
+ };
2622
2767
  }
2623
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
+ };
2624
2790
  const checkoutPrefixOf = (node) => {
2625
2791
  const destination = getInput(node, "path");
2626
2792
  if (destination === void 0) return ".";
@@ -2633,11 +2799,19 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2633
2799
  const value = getInput(node, "sparse-checkout");
2634
2800
  const cone = getInput(node, "sparse-checkout-cone-mode");
2635
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" };
2636
- const paths = value.value.split(/\r?\n/).map((entry) => entry.trim()).filter((entry) => entry !== "");
2637
- 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
+ }
2638
2812
  return {
2639
2813
  mode: "files",
2640
- paths: new Set(paths)
2814
+ paths: literalPaths
2641
2815
  };
2642
2816
  };
2643
2817
  let currentEngineFloor;
@@ -2667,8 +2841,9 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2667
2841
  }
2668
2842
  if (value.startsWith("./")) {
2669
2843
  const selector = staticRepositoryPath(value) ? node_path.default.posix.normalize(value) : void 0;
2670
- const binding = selector === void 0 ? void 0 : checkoutBindings.findLast((entry) => entry.path === "." || selector === entry.path || selector.startsWith(`${entry.path}/`));
2671
- 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({
2672
2847
  rule: "action-pins",
2673
2848
  line,
2674
2849
  message: "local actions require an unambiguous checkout of the current source snapshot"
@@ -2724,24 +2899,55 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2724
2899
  const checkoutRepository = getInput(node, "repository");
2725
2900
  const checkoutRef = getInput(node, "ref");
2726
2901
  const prefix = checkoutPrefixOf(node);
2902
+ const clean = getInput(node, "clean");
2903
+ const cleanIsResolved = clean === void 0 || (0, yaml.isScalar)(clean) && !dynamic(clean.value);
2727
2904
  let repo;
2728
2905
  if (checkoutRepository !== void 0) repo = (0, yaml.isScalar)(checkoutRepository) ? checkoutRepository.value : null;
2729
2906
  const self = repo === void 0 || repo === "${{ github.repository }}" || typeof repo === "string" && repository !== void 0 && repo.toLowerCase() === repository.toLowerCase();
2730
2907
  const workflowSource = repo === "${{ job.workflow_repository }}" && (0, yaml.isScalar)(checkoutRef) && checkoutRef.value === "${{ job.workflow_sha }}";
2731
- const currentSource = self && checkoutRef === void 0;
2732
- 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");
2733
2919
  const dynamicRef = dynamicRefBody(refValue);
2734
- 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;
2735
2929
  if (!staticRepositoryPath(prefix)) checkoutBindings.push({
2736
2930
  path: ".",
2931
+ dynamicSource: validSource && dynamic(prefix),
2737
2932
  source: "untrusted",
2738
2933
  reason: "checkout destination must be a static repository path",
2739
2934
  line,
2740
- sparse: { mode: "invalid" }
2935
+ sparse: sparseCheckoutOf(node)
2741
2936
  });
2742
2937
  else {
2743
2938
  const destination = normalizeCheckoutPath(prefix);
2744
- 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({
2745
2951
  node,
2746
2952
  key: "if"
2747
2953
  }) === void 0 && sourceMap({
@@ -2750,18 +2956,21 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2750
2956
  }) === void 0 && checkoutDestinations.get(destination) === 1 && !unknownCheckoutDestination;
2751
2957
  if (trustedWriter && delegatedSource && typeof refValue === "string") checkoutBindings.push({
2752
2958
  path: destination,
2959
+ dynamicSource: validSource && delegatedSource,
2753
2960
  sparse: sparseCheckoutOf(node),
2754
2961
  source: "delegated",
2755
2962
  ref: refValue
2756
2963
  });
2757
2964
  else if (trustedWriter && (workflowSource || currentSource)) checkoutBindings.push({
2758
2965
  path: destination,
2966
+ dynamicSource: validSource && delegatedSource,
2759
2967
  sparse: sparseCheckoutOf(node),
2760
2968
  source: "tracked"
2761
2969
  });
2762
2970
  else {
2763
2971
  let reason = "checkout does not select a trusted repository source snapshot";
2764
- 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";
2765
2974
  else if (sourceMap({
2766
2975
  node,
2767
2976
  key: "if"
@@ -2772,6 +2981,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2772
2981
  else if (checkoutDestinations.get(destination) !== 1 || unknownCheckoutDestination) reason = "checkout destination must have exactly one static writer";
2773
2982
  checkoutBindings.push({
2774
2983
  path: destination,
2984
+ dynamicSource: validSource && delegatedSource,
2775
2985
  sparse: sparseCheckoutOf(node),
2776
2986
  source: "untrusted",
2777
2987
  reason,
@@ -2820,21 +3030,23 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2820
3030
  line: reference === void 0 ? line : nodeLine(reference)
2821
3031
  });
2822
3032
  };
2823
- const checkContainer = (node) => {
3033
+ const checkContainer = (node, at) => {
2824
3034
  node = resolveNode(node);
2825
3035
  if (node === void 0) return;
2826
3036
  const image = (0, yaml.isMap)(node) ? getNode(node, "image") : node;
2827
3037
  checkRuntimeImage({
2828
3038
  image: (0, yaml.isScalar)(image) ? image.value : void 0,
2829
3039
  line: nodeLine(image ?? node),
2830
- label: "container image"
3040
+ label: "container image",
3041
+ at
2831
3042
  });
2832
3043
  };
2833
- const checkSteps = (node, floor) => {
3044
+ const checkSteps = (node, locationPrefix, floor) => {
2834
3045
  currentEngineFloor = void 0;
2835
3046
  checkoutBindings = [];
2836
3047
  checkoutDestinations = /* @__PURE__ */ new Map();
2837
3048
  unknownCheckoutDestination = false;
3049
+ unreviewableCheckoutSource = false;
2838
3050
  node = resolveNode(node);
2839
3051
  if (!(0, yaml.isSeq)(node)) return;
2840
3052
  for (const step of node.items) {
@@ -2850,9 +3062,14 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2850
3062
  }
2851
3063
  currentEngineFloor = floor;
2852
3064
  try {
2853
- 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
+ }
2854
3070
  } finally {
2855
3071
  currentEngineFloor = void 0;
3072
+ currentLocation = void 0;
2856
3073
  }
2857
3074
  };
2858
3075
  try {
@@ -2866,15 +3083,15 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
2866
3083
  currentEngineFloor = void 0;
2867
3084
  const job = getNode(jobs, key);
2868
3085
  checkAction(job);
2869
- checkContainer(getNode(job, "container"));
3086
+ checkContainer(getNode(job, "container"), `jobs.${key}.container`);
2870
3087
  const services = getNode(job, "services");
2871
- if ((0, yaml.isMap)(services)) for (const service of keysOf(services)) checkContainer(getNode(services, service));
2872
- 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));
2873
3090
  }
2874
3091
  } else {
2875
3092
  const runs = getNode(document.contents, "runs");
2876
3093
  const using = getNode(runs, "using");
2877
- 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");
2878
3095
  if ((0, yaml.isScalar)(using) && using.value === "docker") {
2879
3096
  const image = getNode(runs, "image");
2880
3097
  const value = (0, yaml.isScalar)(image) ? image.value : void 0;
@@ -3103,7 +3320,7 @@ const record = (value) => typeof value === "object" && value !== null && !Array.
3103
3320
  const array = (value) => Array.isArray(value);
3104
3321
  /** Repository-owned declarations are explicit; malformed decisions fail closed. */
3105
3322
  const parseToolchainConfiguration = (input) => {
3106
- 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");
3107
3324
  const disabled = /* @__PURE__ */ new Set();
3108
3325
  const optOuts = input["optOuts"] ?? [];
3109
3326
  if (!array(optOuts)) throw new Error("optOuts must be an array");
@@ -3116,7 +3333,8 @@ const parseToolchainConfiguration = (input) => {
3116
3333
  return {
3117
3334
  disabled,
3118
3335
  cloud: parseCloudSetup(input["cloud"]),
3119
- engineFloors: parseEngineFloors(input["engineFloors"])
3336
+ engineFloors: parseEngineFloors(input["engineFloors"]),
3337
+ dynamicSelectors: parseDynamicSelectors(input["dynamicSelectors"])
3120
3338
  };
3121
3339
  };
3122
3340
  /** Read one tracked configuration snapshot for guards and policy generation. */
@@ -3182,10 +3400,12 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
3182
3400
  let disabled = /* @__PURE__ */ new Set();
3183
3401
  let cloud;
3184
3402
  const engineFloors = [];
3403
+ let dynamicSelectors = [];
3185
3404
  if (files["stll-toolchain.json"] !== void 0) try {
3186
3405
  const configuration = parseToolchainConfiguration(JSON.parse(files["stll-toolchain.json"]));
3187
3406
  disabled = configuration.disabled;
3188
3407
  cloud = configuration.cloud;
3408
+ dynamicSelectors = configuration.dynamicSelectors;
3189
3409
  for (const entry of configuration.engineFloors) engineFloors.push(resolveEngineFloor(entry, files));
3190
3410
  } catch (error) {
3191
3411
  diagnostics.push({
@@ -3225,6 +3445,7 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
3225
3445
  }
3226
3446
  const exercisedFloors = /* @__PURE__ */ new Set();
3227
3447
  const mismatchedFloors = /* @__PURE__ */ new Set();
3448
+ const selectorMatches = /* @__PURE__ */ new Map();
3228
3449
  diagnostics.push(...checkPackageFiles({
3229
3450
  files,
3230
3451
  policy
@@ -3240,6 +3461,8 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
3240
3461
  }
3241
3462
  },
3242
3463
  trackedFiles,
3464
+ dynamicSelectors: dynamicSelectors.filter((entry) => entry.path === file),
3465
+ onDynamicSelector: (entry) => selectorMatches.set(entry, (selectorMatches.get(entry) ?? 0) + 1),
3243
3466
  repository,
3244
3467
  onDelegated,
3245
3468
  engineFloors: engineFloors.filter((entry) => entry.workflow === file),
@@ -3255,6 +3478,12 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
3255
3478
  line: 1,
3256
3479
  message: `engine floor ${entry.workflow}:${entry.job} requires a setup-node exact patch satisfying ${entry.package} engines.node minimum major ${entry.major}`
3257
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
+ });
3258
3487
  diagnostics.push(...checkDependabot({
3259
3488
  files,
3260
3489
  policy: policy.dependabot