@stll/oxlint-config 0.14.1 → 1.0.0

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.
@@ -12,9 +12,9 @@ picomatch = require_rolldown_runtime.__toESM(picomatch, 1);
12
12
  let smol_toml = require("smol-toml");
13
13
  //#endregion
14
14
  //#region src/toolchain-consumer-checks.ts
15
- const consumerRunnerVersion = "0.14.1";
15
+ const consumerRunnerVersion = "1.0.0";
16
16
  const consumerCheckWorkflow = "stella/.github/.github/workflows/package-consumer-compat.yml";
17
- const record$6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
17
+ const record$7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
18
18
  const repositoryDirectory = (value) => typeof value === "string" && value !== "" && !value.includes("\\") && !value.includes("${{") && !value.split("").some((character) => {
19
19
  const code = character.charCodeAt(0);
20
20
  return code < 32 || code === 127;
@@ -100,12 +100,12 @@ const validConsumerCron = (text) => {
100
100
  });
101
101
  };
102
102
  const nightlySchedule = (value) => {
103
- if (!record$6(value)) return false;
103
+ if (!record$7(value)) return false;
104
104
  const schedule = value["schedule"];
105
105
  if (!Array.isArray(schedule) || schedule.length === 0) return false;
106
106
  const coveredDays = /* @__PURE__ */ new Set();
107
107
  for (const entry of schedule) {
108
- if (!record$6(entry) || typeof entry["cron"] !== "string" || !validConsumerCron(entry["cron"])) return false;
108
+ if (!record$7(entry) || typeof entry["cron"] !== "string" || !validConsumerCron(entry["cron"])) return false;
109
109
  const fields = entry["cron"].trim().split(/\s+/);
110
110
  const selections = cronFields.map((field, index) => {
111
111
  const text = fields.at(index);
@@ -121,7 +121,7 @@ const parseConsumerChecks = (input) => {
121
121
  if (!Array.isArray(input)) throw new Error("consumerChecks must be an array");
122
122
  const targets = /* @__PURE__ */ new Set();
123
123
  return input.map((entry) => {
124
- if (!record$6(entry) || Object.keys(entry).some((key) => ![
124
+ if (!record$7(entry) || Object.keys(entry).some((key) => ![
125
125
  "workflow",
126
126
  "job",
127
127
  "packages",
@@ -159,9 +159,9 @@ const checkConsumerChecks = ({ declarations, files, policy }) => {
159
159
  });
160
160
  if (document.errors.length > 0) throw new Error("invalid workflow YAML");
161
161
  const source = document.toJS({ maxAliasCount: 100 });
162
- if (!record$6(source) || !record$6(source["jobs"])) continue;
162
+ if (!record$7(source) || !record$7(source["jobs"])) continue;
163
163
  for (const [jobName, job] of Object.entries(source["jobs"])) {
164
- if (!record$6(job) || typeof job["uses"] !== "string") continue;
164
+ if (!record$7(job) || typeof job["uses"] !== "string") continue;
165
165
  const [workflowPath, ref] = job["uses"].split("@");
166
166
  if (workflowPath?.toLowerCase() !== "stella/.github/.github/workflows/package-consumer-compat.yml") continue;
167
167
  const declaration = declarations.find((entry) => entry.workflow === workflow && entry.job === jobName);
@@ -175,7 +175,7 @@ const checkConsumerChecks = ({ declarations, files, policy }) => {
175
175
  const approved = Object.entries(policy.actions).find(([name]) => name.toLowerCase() === consumerCheckWorkflow)?.[1];
176
176
  if (job["uses"].split("@").length !== 2 || ref === void 0 || !/^[a-f0-9]{40}$/.test(ref) || approved === void 0 || ref !== approved.sha || "steps" in job || "runs-on" in job) throw new Error(`${workflow}:${jobName} must invoke the approved immutable consumer workflow as a reusable job`);
177
177
  const inputs = job["with"];
178
- if (!record$6(inputs)) throw new Error(`${workflow}:${jobName} requires consumer inputs`);
178
+ if (!record$7(inputs)) throw new Error(`${workflow}:${jobName} requires consumer inputs`);
179
179
  const names = /* @__PURE__ */ new Set();
180
180
  for (const key of Object.keys(inputs)) {
181
181
  const normalized = key.toLowerCase();
@@ -259,10 +259,12 @@ const canonicalDockerRuntime = (image) => {
259
259
  const registry = dockerHubRegistries.find((entry) => image.toLowerCase().startsWith(`${entry}/`));
260
260
  return (registry === void 0 ? image : image.slice(registry.length + 1)).replace(/^library\//i, "");
261
261
  };
262
+ /** Preserve Rust release identity across official Debian slim and Alpine variants. */
263
+ const rustImageVersion = (tag) => tag.replace(/-(?:alpine(?:\d+(?:\.\d+)*)?|(?:bookworm|bullseye|trixie|buster)(?:-slim)?|slim(?:-(?:bookworm|bullseye|trixie|buster))?)$/, "");
262
264
  //#endregion
263
265
  //#region src/toolchain-dependabot.ts
264
266
  const dependabotRules = ["dependabot-policy"];
265
- const record$5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
267
+ const record$6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
266
268
  const strings = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
267
269
  const sameStrings = (value, expected) => {
268
270
  if (!strings(value) || value.length !== expected.length) return false;
@@ -284,8 +286,8 @@ const workspaceTable = (parsed, ecosystem) => {
284
286
  case "cargo": return parsed["workspace"];
285
287
  case "uv": {
286
288
  const tool = parsed["tool"];
287
- const uv = record$5(tool) ? tool["uv"] : void 0;
288
- return record$5(uv) ? uv["workspace"] : void 0;
289
+ const uv = record$6(tool) ? tool["uv"] : void 0;
290
+ return record$6(uv) ? uv["workspace"] : void 0;
289
291
  }
290
292
  default: throw new Error("unknown workspace ecosystem", { cause: ecosystem });
291
293
  }
@@ -305,7 +307,7 @@ const workspaceRoots = (files, ecosystem) => {
305
307
  const document = (0, yaml.parseDocument)(text);
306
308
  if (document.errors.length > 0) throw new Error(`${workspaceFile}:1: invalid pnpm workspace YAML`);
307
309
  const parsed = document.toJS({ maxAliasCount: 100 });
308
- if (!record$5(parsed)) throw new Error(`${workspaceFile}:1: pnpm workspace must contain an object`);
310
+ if (!record$6(parsed)) throw new Error(`${workspaceFile}:1: pnpm workspace must contain an object`);
309
311
  const patterns = parsed["packages"] ?? [];
310
312
  if (!strings(patterns)) throw new Error(`${workspaceFile}:1: pnpm packages must be a string array`);
311
313
  workspaces.set(directory, patterns.map((pattern) => pattern.startsWith("!") ? `!${node_path.default.posix.normalize(pattern.slice(1))}` : node_path.default.posix.normalize(pattern)));
@@ -317,13 +319,13 @@ const workspaceRoots = (files, ecosystem) => {
317
319
  }
318
320
  try {
319
321
  const parsed = ecosystem === "npm" ? JSON.parse(files[file] ?? "") : (0, smol_toml.parse)(files[file] ?? "");
320
- if (!record$5(parsed)) continue;
322
+ if (!record$6(parsed)) continue;
321
323
  if (ecosystem === "npm" && typeof parsed["packageManager"] === "string" && parsed["packageManager"].startsWith("pnpm@")) continue;
322
324
  const value = workspaceTable(parsed, ecosystem);
323
- const patterns = record$5(value) ? value[ecosystem === "npm" ? "packages" : "members"] : value;
324
- const members = ecosystem === "cargo" && record$5(value) && patterns === void 0 ? [] : patterns;
325
+ const patterns = record$6(value) ? value[ecosystem === "npm" ? "packages" : "members"] : value;
326
+ const members = ecosystem === "cargo" && record$6(value) && patterns === void 0 ? [] : patterns;
325
327
  if (strings(members)) {
326
- const excluded = ecosystem !== "npm" && record$5(value) && strings(value["exclude"]) ? value["exclude"].map((pattern) => `!${pattern}`) : [];
328
+ const excluded = ecosystem !== "npm" && record$6(value) && strings(value["exclude"]) ? value["exclude"].map((pattern) => `!${pattern}`) : [];
327
329
  workspaces.set(node_path.default.posix.dirname(file), [...members, ...excluded]);
328
330
  }
329
331
  } catch {}
@@ -374,11 +376,11 @@ const workspaceRoots = (files, ecosystem) => {
374
376
  for (const directory of memberDirectories) try {
375
377
  const parsed = (0, smol_toml.parse)(files[node_path.default.posix.join(directory, "Cargo.toml")] ?? "");
376
378
  const workspace = (0, smol_toml.parse)(files[node_path.default.posix.join(root, "Cargo.toml")] ?? "")["workspace"];
377
- const inherited = record$5(workspace) ? workspace["dependencies"] : void 0;
379
+ const inherited = record$6(workspace) ? workspace["dependencies"] : void 0;
378
380
  const dependencyOwners = [parsed];
379
381
  const targets = parsed["target"];
380
- if (record$5(targets)) {
381
- for (const target of Object.values(targets)) if (record$5(target)) dependencyOwners.push(target);
382
+ if (record$6(targets)) {
383
+ for (const target of Object.values(targets)) if (record$6(target)) dependencyOwners.push(target);
382
384
  }
383
385
  for (const owner of dependencyOwners) for (const key of [
384
386
  "dependencies",
@@ -386,12 +388,12 @@ const workspaceRoots = (files, ecosystem) => {
386
388
  "build-dependencies"
387
389
  ]) {
388
390
  const dependencies = owner[key];
389
- if (!record$5(dependencies)) continue;
391
+ if (!record$6(dependencies)) continue;
390
392
  for (const [name, dependency] of Object.entries(dependencies)) {
391
- if (!record$5(dependency)) continue;
393
+ if (!record$6(dependency)) continue;
392
394
  const isInherited = dependency["workspace"] === true;
393
- const resolved = isInherited && record$5(inherited) ? inherited[name] : dependency;
394
- if (!record$5(resolved) || typeof resolved["path"] !== "string") continue;
395
+ const resolved = isInherited && record$6(inherited) ? inherited[name] : dependency;
396
+ if (!record$6(resolved) || typeof resolved["path"] !== "string") continue;
395
397
  if (node_path.default.posix.isAbsolute(resolved["path"]) || /\\|\$|^[A-Za-z]:/.test(resolved["path"])) continue;
396
398
  const target = node_path.default.posix.normalize(node_path.default.posix.join(isInherited ? root : directory, resolved["path"]));
397
399
  const relative = node_path.default.posix.relative(root, target);
@@ -419,7 +421,7 @@ const javascriptEcosystem = (directory, files) => {
419
421
  if (files[node_path.default.posix.join(root, "bun.lock")] !== void 0) return "bun";
420
422
  try {
421
423
  const manifest = JSON.parse(files[node_path.default.posix.join(root, "package.json")] ?? "{}");
422
- if (record$5(manifest) && typeof manifest["packageManager"] === "string" && manifest["packageManager"].startsWith("bun@")) return "bun";
424
+ if (record$6(manifest) && typeof manifest["packageManager"] === "string" && manifest["packageManager"].startsWith("bun@")) return "bun";
423
425
  } catch {}
424
426
  return "npm";
425
427
  };
@@ -431,7 +433,7 @@ const pythonEcosystem = (file, files) => {
431
433
  const pyproject = files[node_path.default.posix.join(root, "pyproject.toml")];
432
434
  if (pyproject !== void 0) try {
433
435
  const tool = (0, smol_toml.parse)(pyproject)["tool"];
434
- if (record$5(tool) && record$5(tool["uv"])) return "uv";
436
+ if (record$6(tool) && record$6(tool["uv"])) return "uv";
435
437
  } catch {}
436
438
  return "pip";
437
439
  };
@@ -502,7 +504,7 @@ const checkDependabot = ({ files, policy }) => {
502
504
  add("cannot resolve Dependabot YAML aliases");
503
505
  return diagnostics;
504
506
  }
505
- if (!record$5(parsed) || parsed["version"] !== 2 || !Array.isArray(parsed["updates"])) {
507
+ if (!record$6(parsed) || parsed["version"] !== 2 || !Array.isArray(parsed["updates"])) {
506
508
  add("Dependabot must declare version: 2 and an updates array");
507
509
  return diagnostics;
508
510
  }
@@ -515,7 +517,7 @@ const checkDependabot = ({ files, policy }) => {
515
517
  });
516
518
  if (legacyDocument.errors.length === 0) legacy = legacyDocument.toJS({ maxAliasCount: 100 });
517
519
  } catch {}
518
- const legacyUpdates = record$5(legacy) && Array.isArray(legacy["updates"]) ? legacy["updates"] : [];
520
+ const legacyUpdates = record$6(legacy) && Array.isArray(legacy["updates"]) ? legacy["updates"] : [];
519
521
  const rootClaims = /* @__PURE__ */ new Map();
520
522
  const directoryClaims = /* @__PURE__ */ new Map();
521
523
  for (const [index, update] of parsed["updates"].entries()) {
@@ -529,7 +531,7 @@ const checkDependabot = ({ files, policy }) => {
529
531
  return offset === void 0 ? 1 : lineCounter.linePos(offset).line;
530
532
  };
531
533
  const report = (key, message) => add(message, lineOf(key));
532
- if (!record$5(update) || typeof update["package-ecosystem"] !== "string") {
534
+ if (!record$6(update) || typeof update["package-ecosystem"] !== "string") {
533
535
  report("package-ecosystem", "each Dependabot update must declare a package ecosystem");
534
536
  continue;
535
537
  }
@@ -563,21 +565,21 @@ const checkDependabot = ({ files, policy }) => {
563
565
  covered.set(ecosystem, matches);
564
566
  const schedule = update["schedule"];
565
567
  const legacyUpdate = legacyUpdates.at(index);
566
- const legacySchedule = record$5(legacyUpdate) ? legacyUpdate["schedule"] : void 0;
567
- 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`);
568
- 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`);
568
+ const legacySchedule = record$6(legacyUpdate) ? legacyUpdate["schedule"] : void 0;
569
+ 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`);
570
+ 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`);
569
571
  const cooldown = update["cooldown"];
570
- 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}`);
572
+ 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}`);
571
573
  const groups = update["groups"];
572
- if (!record$5(groups) || Object.keys(groups).length !== Object.keys(policy.groups).length || Object.entries(policy.groups).some(([name, expected]) => {
574
+ if (!record$6(groups) || Object.keys(groups).length !== Object.keys(policy.groups).length || Object.entries(policy.groups).some(([name, expected]) => {
573
575
  const group = groups[name];
574
- return !record$5(group) || Object.keys(group).length !== 2 || !sameStrings(group["patterns"], expected.patterns) || !sameStrings(group["update-types"], expected.updateTypes);
576
+ return !record$6(group) || Object.keys(group).length !== 2 || !sameStrings(group["patterns"], expected.patterns) || !sameStrings(group["update-types"], expected.updateTypes);
575
577
  })) report("groups", `${ecosystem}: groups must match the shared policy`);
576
578
  const ignored = ignoredFor(ecosystem, policy);
577
579
  const ignore = update["ignore"];
578
580
  if (ignore !== void 0 && (!Array.isArray(ignore) || ignore.length === 0)) report("ignore", `${ecosystem}: omit ignore or provide a nonempty array`);
579
- 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`);
580
- 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}`);
581
+ 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`);
582
+ 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}`);
581
583
  }
582
584
  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}`);
583
585
  return diagnostics;
@@ -589,7 +591,7 @@ const locatorValidators = {
589
591
  "bun-source": /^(?:jobs\.[\w-]+|runs)\.steps\.[\w-]+$/
590
592
  };
591
593
  const knownKind = (value) => typeof value === "string" && Object.hasOwn(locatorValidators, value);
592
- const record$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
594
+ const record$5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
593
595
  /** One reviewed declaration identifies one unresolved selector, never a rule. */
594
596
  const parseDynamicSelectors = (input) => {
595
597
  if (input === void 0) return [];
@@ -597,7 +599,7 @@ const parseDynamicSelectors = (input) => {
597
599
  const result = [];
598
600
  const keys = /* @__PURE__ */ new Set();
599
601
  for (const entry of input) {
600
- if (!record$4(entry) || Object.keys(entry).some((key) => ![
602
+ if (!record$5(entry) || Object.keys(entry).some((key) => ![
601
603
  "path",
602
604
  "kind",
603
605
  "reason",
@@ -633,7 +635,7 @@ const parseDynamicSelectors = (input) => {
633
635
  };
634
636
  //#endregion
635
637
  //#region src/toolchain-engine-floors.ts
636
- const record$3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
638
+ const record$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
637
639
  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 !== "..");
638
640
  const parseEngineFloors = (input) => {
639
641
  if (input === void 0) return [];
@@ -641,7 +643,7 @@ const parseEngineFloors = (input) => {
641
643
  const result = [];
642
644
  const jobs = /* @__PURE__ */ new Set();
643
645
  for (const entry of input) {
644
- if (!record$3(entry) || Object.keys(entry).some((key) => ![
646
+ if (!record$4(entry) || Object.keys(entry).some((key) => ![
645
647
  "package",
646
648
  "workflow",
647
649
  "job"
@@ -662,16 +664,16 @@ const resolveEngineFloor = (entry, files) => {
662
664
  const workflowText = files[entry.workflow];
663
665
  if (manifestText === void 0 || workflowText === void 0) throw new Error("engine floor package and workflow must be tracked");
664
666
  const manifest = JSON.parse(manifestText);
665
- if (!record$3(manifest) || manifest["private"] === true || typeof manifest["name"] !== "string" || manifest["name"].trim() === "" || !record$3(manifest["engines"]) || typeof manifest["engines"]["node"] !== "string") throw new Error("engine floor package must be published and declare engines.node");
667
+ if (!record$4(manifest) || manifest["private"] === true || typeof manifest["name"] !== "string" || manifest["name"].trim() === "" || !record$4(manifest["engines"]) || typeof manifest["engines"]["node"] !== "string") throw new Error("engine floor package must be published and declare engines.node");
666
668
  const range = manifest["engines"]["node"];
667
669
  const minimum = (0, semver.validRange)(range) === null ? null : (0, semver.minVersion)(range);
668
670
  if (minimum === null || minimum.prerelease.length !== 0) throw new Error("engine floor engines.node must declare a stable minimum");
669
671
  const document = (0, yaml.parseDocument)(workflowText, { merge: true });
670
672
  if (document.errors.length !== 0) throw new Error("engine floor workflow must be valid YAML");
671
673
  const workflow = document.toJS({ maxAliasCount: 100 });
672
- if (!record$3(workflow) || !record$3(workflow["jobs"])) throw new Error("engine floor job must exist in its tracked workflow");
674
+ if (!record$4(workflow) || !record$4(workflow["jobs"])) throw new Error("engine floor job must exist in its tracked workflow");
673
675
  const job = workflow["jobs"][entry.job];
674
- if (!record$3(job)) throw new Error("engine floor job must exist in its tracked workflow");
676
+ if (!record$4(job)) throw new Error("engine floor job must exist in its tracked workflow");
675
677
  if (!Array.isArray(job["steps"]) || Object.hasOwn(job, "uses")) throw new Error("engine floor job must declare steps and cannot reuse a workflow");
676
678
  return {
677
679
  ...entry,
@@ -691,7 +693,7 @@ const packageRules = [
691
693
  "typescript-layout",
692
694
  "node-engine"
693
695
  ];
694
- const record$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
696
+ const record$3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
695
697
  const shellSubstitutions = (command) => {
696
698
  const frames = [{
697
699
  kind: "root",
@@ -752,43 +754,371 @@ const shellSubstitutions = (command) => {
752
754
  for (const frame of frames.slice(1)) bodies.push(command.slice(frame.start));
753
755
  return bodies;
754
756
  };
755
- const compilerCommandSegments = (command) => {
756
- const segments = [[]];
757
- for (const source of [command, ...shellSubstitutions(command)]) {
758
- segments.push([]);
759
- const shellWords = source.match(/(?:[^\s"'\\;&|()]|\\[\s\S]|"(?:[^"\\]|\\[\s\S])*"|'[^']*')+|&&|\|\||[();&|\n]/g);
760
- for (const word of shellWords ?? []) if (/^(?:&&|\|\||[;&|\n])$/.test(word)) segments.push([]);
761
- else segments.at(-1)?.push(word);
757
+ const shellValueOptions = /* @__PURE__ */ new Set([
758
+ "--rcfile",
759
+ "--init-file",
760
+ "-o",
761
+ "+o",
762
+ "-O",
763
+ "+O"
764
+ ]);
765
+ const shellExecutable = /^(?:\/(?:[^/]+\/)*)?(?:bash|dash|ksh|zsh|sh)$/;
766
+ const shellTokens = (source) => {
767
+ const tokens = [];
768
+ for (let index = 0; index < source.length;) {
769
+ const character = source.at(index);
770
+ if (character === void 0) break;
771
+ if (character === "\n") {
772
+ tokens.push({
773
+ kind: "boundary",
774
+ value: character,
775
+ index: index++
776
+ });
777
+ continue;
778
+ }
779
+ if (/\s/.test(character)) {
780
+ index++;
781
+ continue;
782
+ }
783
+ if (character === "#") {
784
+ const end = source.indexOf("\n", index);
785
+ const stop = end < 0 ? source.length : end;
786
+ tokens.push({
787
+ kind: "comment",
788
+ value: source.slice(index, stop),
789
+ index
790
+ });
791
+ index = stop;
792
+ continue;
793
+ }
794
+ const redirect = /^(?:\d*)(?:&>>|&>|<<<|<<-|<<|>>|>\||>&|<&|<>|>|<)/.exec(source.slice(index));
795
+ if (redirect !== null) {
796
+ tokens.push({
797
+ kind: "redirect",
798
+ value: redirect[0],
799
+ index
800
+ });
801
+ index += redirect[0].length;
802
+ continue;
803
+ }
804
+ const boundary = /^(?:&&|\|\||[();&|])/.exec(source.slice(index));
805
+ if (boundary !== null) {
806
+ tokens.push({
807
+ kind: "boundary",
808
+ value: boundary[0],
809
+ index
810
+ });
811
+ index += boundary[0].length;
812
+ continue;
813
+ }
814
+ const start = index;
815
+ const frames = [{
816
+ kind: "word",
817
+ quote: void 0,
818
+ depth: 0
819
+ }];
820
+ while (index < source.length) {
821
+ const frame = frames.at(-1);
822
+ const current = source.at(index);
823
+ if (frame === void 0 || current === void 0) break;
824
+ if (current === "\\" && frame.quote !== "'") {
825
+ index += 2;
826
+ continue;
827
+ }
828
+ if (current === "`" && frame.kind === "backticks") {
829
+ frames.pop();
830
+ index++;
831
+ continue;
832
+ }
833
+ if (current === frame.quote) {
834
+ frame.quote = void 0;
835
+ index++;
836
+ continue;
837
+ }
838
+ if (frame.quote === "'") {
839
+ index++;
840
+ continue;
841
+ }
842
+ if (current === "$" && source.at(index + 1) === "(") {
843
+ frames.push({
844
+ kind: "parentheses",
845
+ quote: void 0,
846
+ depth: 1
847
+ });
848
+ index += 2;
849
+ continue;
850
+ }
851
+ if (current === "`") {
852
+ frames.push({
853
+ kind: "backticks",
854
+ quote: void 0,
855
+ depth: 0
856
+ });
857
+ index++;
858
+ continue;
859
+ }
860
+ if (frame.quote !== void 0) {
861
+ index++;
862
+ continue;
863
+ }
864
+ if (current === "'" || current === "\"") {
865
+ frame.quote = current;
866
+ index++;
867
+ continue;
868
+ }
869
+ if (frame.kind === "parentheses") {
870
+ if (current === "(") frame.depth++;
871
+ if (current === ")" && --frame.depth === 0) frames.pop();
872
+ index++;
873
+ continue;
874
+ }
875
+ if (frame.kind === "word" && /[\s<>;&|()]/.test(current)) break;
876
+ index++;
877
+ }
878
+ const value = source.slice(start, index);
879
+ tokens.push({
880
+ kind: "word",
881
+ value,
882
+ index: start
883
+ });
762
884
  }
763
- return segments.map((words) => {
764
- let start = 0;
765
- const skipAssignments = () => {
766
- while (/^[A-Za-z_][A-Za-z\d_]*=/.test(words.at(start) ?? "")) start += 1;
767
- };
768
- skipAssignments();
769
- while (/^(?:\/usr\/bin\/|\/bin\/)?env$/.test(words.at(start) ?? "")) {
770
- start += 1;
771
- while (start < words.length) {
772
- const option = words.at(start);
773
- if (option === "--") {
774
- start += 1;
775
- break;
776
- }
777
- if (option === "-u" || option === "--unset") {
885
+ let groupDepth = 0;
886
+ let redirectTarget = false;
887
+ const commandWords = [];
888
+ for (const token of tokens) {
889
+ if (token.kind === "comment") continue;
890
+ if (token.kind === "redirect") {
891
+ redirectTarget = true;
892
+ continue;
893
+ }
894
+ if (redirectTarget) {
895
+ redirectTarget = false;
896
+ if (token.kind === "word") continue;
897
+ }
898
+ if (token.kind === "boundary") {
899
+ commandWords.length = 0;
900
+ continue;
901
+ }
902
+ const functionBody = commandWords.length === 2 && commandWords.at(0) === "function";
903
+ const opensGroup = token.value === "{" && (commandWords.length === 0 || functionBody);
904
+ const closesGroup = token.value === "}" && groupDepth > 0 && commandWords.length === 0;
905
+ if (opensGroup || closesGroup) {
906
+ token.kind = "boundary";
907
+ groupDepth += opensGroup ? 1 : -1;
908
+ commandWords.length = 0;
909
+ continue;
910
+ }
911
+ if (commandWords.length === 0 && [
912
+ "if",
913
+ "then",
914
+ "elif",
915
+ "else",
916
+ "do",
917
+ "!",
918
+ "time"
919
+ ].includes(token.value)) continue;
920
+ commandWords.push(token.value);
921
+ }
922
+ return tokens;
923
+ };
924
+ const shellArgumentTokens = (tokens) => {
925
+ const arguments_ = [];
926
+ let redirectTarget = false;
927
+ for (const token of tokens) {
928
+ if (token.kind === "comment") continue;
929
+ if (token.kind === "redirect") {
930
+ redirectTarget = true;
931
+ continue;
932
+ }
933
+ if (redirectTarget) {
934
+ redirectTarget = false;
935
+ if (token.kind === "word") continue;
936
+ }
937
+ arguments_.push(token);
938
+ }
939
+ return arguments_;
940
+ };
941
+ const shellReadsProgramStdin = (tokens) => {
942
+ const words = executableShellWords(shellArgumentTokens(tokens).map(({ value }) => value)).map(decodeShellWord);
943
+ if (!shellExecutable.test(words.at(0) ?? "")) return false;
944
+ let stdin = false;
945
+ let script = false;
946
+ for (let index = 1; index < words.length; index++) {
947
+ const option = words.at(index) ?? "";
948
+ if (option === "--" || option === "-") {
949
+ script = index + 1 < words.length;
950
+ break;
951
+ }
952
+ if (!/^[+-]/.test(option)) {
953
+ script = true;
954
+ break;
955
+ }
956
+ if (shellValueOptions.has(option)) {
957
+ index++;
958
+ continue;
959
+ }
960
+ if (/^-[^-]*c/.test(option)) return false;
961
+ if (/^-[^-]*s/.test(option)) stdin = true;
962
+ }
963
+ return stdin || !script;
964
+ };
965
+ const shellStdinHeredoc = (tokens) => {
966
+ const descriptors = /* @__PURE__ */ new Map();
967
+ for (let index = 0; index < tokens.length; index++) {
968
+ const token = tokens.at(index);
969
+ if (token?.kind !== "redirect") continue;
970
+ const declaration = /^(\d*)(.+)$/.exec(token.value);
971
+ const operator = declaration?.[2];
972
+ if (operator === void 0) continue;
973
+ const descriptor = String(Number(declaration?.[1] || (operator.startsWith("<") ? "0" : "1")));
974
+ const target = tokens.at(++index);
975
+ if (operator === "<<" || operator === "<<-") {
976
+ descriptors.set(descriptor, token);
977
+ continue;
978
+ }
979
+ if (operator === "<&" || operator === ">&") {
980
+ const copy = /^(\d+)(-?)$/.exec(decodeShellWord(target?.value ?? ""));
981
+ const sourceDescriptor = copy?.[1] === void 0 ? void 0 : String(Number(copy[1]));
982
+ const source = sourceDescriptor === void 0 ? void 0 : descriptors.get(sourceDescriptor);
983
+ if (source === void 0) descriptors.delete(descriptor);
984
+ else descriptors.set(descriptor, source);
985
+ if (copy?.[2] === "-" && sourceDescriptor !== void 0) descriptors.delete(sourceDescriptor);
986
+ continue;
987
+ }
988
+ descriptors.delete(descriptor);
989
+ if (operator === "&>" || operator === "&>>") descriptors.delete("2");
990
+ }
991
+ return descriptors.get("0");
992
+ };
993
+ const shellExecutableSource = (command) => {
994
+ const pending = [];
995
+ return command.split(/\r?\n/).map((line) => {
996
+ const here = pending.at(0);
997
+ if (here !== void 0) {
998
+ if ((here.stripTabs ? line.replace(/^\t+/, "") : line) === here.delimiter) {
999
+ pending.shift();
1000
+ return "";
1001
+ }
1002
+ if (here.execute) return line;
1003
+ if (here.literal) return "";
1004
+ return shellSubstitutions(line).join("\n");
1005
+ }
1006
+ const words = shellTokens(line);
1007
+ let commandStart = 0;
1008
+ for (let index = 0; index < words.length; index++) {
1009
+ const match = words.at(index);
1010
+ const word = match?.value;
1011
+ if (word === void 0) continue;
1012
+ if (match?.kind === "comment") return line.slice(0, match.index);
1013
+ if (match?.kind === "boundary") {
1014
+ commandStart = index + 1;
1015
+ continue;
1016
+ }
1017
+ const declaration = match?.kind === "redirect" ? /^(?:\d*)<<(-?)(.*)$/.exec(word) : null;
1018
+ let delimiter = declaration?.[2];
1019
+ if (delimiter === "") delimiter = words.at(++index)?.value;
1020
+ if (declaration !== null && delimiter !== void 0 && !delimiter.startsWith("<")) {
1021
+ const commandEnd = words.findIndex((entry, position) => position > index && (entry.kind === "boundary" || entry.kind === "comment"));
1022
+ const invocation = words.slice(commandStart, commandEnd < 0 ? void 0 : commandEnd);
1023
+ pending.push({
1024
+ delimiter: decodeShellWord(delimiter),
1025
+ stripTabs: declaration[1] === "-",
1026
+ literal: /['"\\]/.test(delimiter),
1027
+ execute: shellReadsProgramStdin(invocation) && shellStdinHeredoc(invocation) === match
1028
+ });
1029
+ }
1030
+ }
1031
+ return line;
1032
+ }).join("\n");
1033
+ };
1034
+ const executableShellWords = (words) => {
1035
+ let start = 0;
1036
+ const skipAssignments = () => {
1037
+ while (/^[A-Za-z_][A-Za-z\d_]*=/.test(decodeShellWord(words.at(start) ?? ""))) start += 1;
1038
+ };
1039
+ skipAssignments();
1040
+ while (/^(?:\/usr\/bin\/|\/bin\/)?env$/.test(decodeShellWord(words.at(start) ?? ""))) {
1041
+ start += 1;
1042
+ while (start < words.length) {
1043
+ const option = decodeShellWord(words.at(start) ?? "");
1044
+ if (option === "--") {
1045
+ start += 1;
1046
+ break;
1047
+ }
1048
+ if ([
1049
+ "-u",
1050
+ "--unset",
1051
+ "-C",
1052
+ "--chdir"
1053
+ ].includes(option)) {
1054
+ start += 2;
1055
+ continue;
1056
+ }
1057
+ if (option === "-S" || option === "--split-string" || option.startsWith("--split-string=") || option.startsWith("-S") && option.length > 2) {
1058
+ let source;
1059
+ if (option === "-S" || option === "--split-string") {
1060
+ source = decodeShellWord(words.at(start + 1) ?? "");
778
1061
  start += 2;
779
- continue;
1062
+ } else {
1063
+ source = option.startsWith("-S") ? option.slice(2) : option.slice(15);
1064
+ start++;
780
1065
  }
781
- if (option === "-" || option === "-i" || option === "--ignore-environment" || /^--unset=.+|^-u.+/.test(option ?? "")) {
782
- start += 1;
783
- continue;
784
- }
785
- break;
1066
+ return executableShellWords([
1067
+ "env",
1068
+ ...shellTokens(source).map(({ value }) => value),
1069
+ ...words.slice(start)
1070
+ ]);
786
1071
  }
787
- skipAssignments();
1072
+ if (option === "-" || option === "-i" || option === "--ignore-environment" || /^--(?:unset|chdir)=.+|^-[uC].+/.test(option)) {
1073
+ start += 1;
1074
+ continue;
1075
+ }
1076
+ break;
788
1077
  }
789
- return words.slice(start);
790
- });
1078
+ skipAssignments();
1079
+ }
1080
+ return words.slice(start);
1081
+ };
1082
+ const shellCommandBody = (rawWords) => {
1083
+ const words = executableShellWords(rawWords.filter((word) => word !== "(" && word !== ")")).map(decodeShellWord);
1084
+ if (!shellExecutable.test(words.at(0) ?? "")) return void 0;
1085
+ for (let index = 1; index < words.length; index++) {
1086
+ const option = words.at(index);
1087
+ if (option === void 0 || option === "--" || !/^[+-]/.test(option)) return void 0;
1088
+ if (shellValueOptions.has(option)) {
1089
+ index++;
1090
+ continue;
1091
+ }
1092
+ if (/^-[^-]*c/.test(option)) return words.at(index + 1);
1093
+ }
1094
+ };
1095
+ /** Shared executable shell words preserve assignments for runtime selectors. */
1096
+ const shellCommandSegments = (command, grouping = "boundaries") => {
1097
+ const segments = [[]];
1098
+ const executableSource = shellExecutableSource(command);
1099
+ const sources = /* @__PURE__ */ new Set([executableSource, ...shellSubstitutions(executableSource)]);
1100
+ for (const source of sources) {
1101
+ const firstSegment = segments.length;
1102
+ segments.push([]);
1103
+ for (const { kind, value: word } of shellArgumentTokens(shellTokens(source))) if (kind === "boundary") if ([
1104
+ "(",
1105
+ ")",
1106
+ "{",
1107
+ "}"
1108
+ ].includes(word) && grouping === "tokens") segments.at(-1)?.push(word);
1109
+ else segments.push([]);
1110
+ else segments.at(-1)?.push(word);
1111
+ for (const words of segments.slice(firstSegment)) {
1112
+ const body = shellCommandBody(words);
1113
+ if (body === void 0) continue;
1114
+ const executableBody = shellExecutableSource(body);
1115
+ sources.add(executableBody);
1116
+ for (const substitution of shellSubstitutions(executableBody)) sources.add(substitution);
1117
+ }
1118
+ }
1119
+ return segments;
791
1120
  };
1121
+ const compilerCommandSegments = (command) => shellCommandSegments(command, "tokens").map(executableShellWords);
792
1122
  const launcherValueOptions = new Map(Object.entries({
793
1123
  npx: [
794
1124
  "-p",
@@ -922,10 +1252,10 @@ const checkPackageFiles = ({ files, policy }) => {
922
1252
  const document = pnpm ? (0, yaml.parseDocument)(text, { lineCounter }) : void 0;
923
1253
  if (document !== void 0 && document.errors.length > 0) throw new Error("invalid YAML");
924
1254
  const json = document === void 0 ? JSON.parse(text) : document.toJS();
925
- if (!record$2(json)) throw new Error("manifest must be an object");
1255
+ if (!record$3(json)) throw new Error("manifest must be an object");
926
1256
  if (pnpm) {
927
- if (json["catalog"] !== void 0 && !record$2(json["catalog"])) throw new Error("catalog must be a mapping");
928
- if (json["catalogs"] !== void 0 && (!record$2(json["catalogs"]) || Object.values(json["catalogs"]).some((catalog) => !record$2(catalog)))) throw new Error("named catalogs must be mappings");
1257
+ if (json["catalog"] !== void 0 && !record$3(json["catalog"])) throw new Error("catalog must be a mapping");
1258
+ if (json["catalogs"] !== void 0 && (!record$3(json["catalogs"]) || Object.values(json["catalogs"]).some((catalog) => !record$3(catalog)))) throw new Error("named catalogs must be mappings");
929
1259
  if (json["packages"] !== void 0 && (!Array.isArray(json["packages"]) || !json["packages"].every((pattern) => typeof pattern === "string"))) throw new Error("workspace packages must be patterns");
930
1260
  }
931
1261
  manifests.set(file, json);
@@ -939,13 +1269,13 @@ const checkPackageFiles = ({ files, policy }) => {
939
1269
  catalog: json["overrides"],
940
1270
  prefix: ["overrides"]
941
1271
  },
942
- ...record$2(json["catalogs"]) ? Object.entries(json["catalogs"]).map(([name, catalog]) => ({
1272
+ ...record$3(json["catalogs"]) ? Object.entries(json["catalogs"]).map(([name, catalog]) => ({
943
1273
  catalog,
944
1274
  prefix: ["catalogs", name]
945
1275
  })) : []
946
1276
  ];
947
1277
  for (const { catalog, prefix } of catalogEntries) {
948
- if (!record$2(catalog)) continue;
1278
+ if (!record$3(catalog)) continue;
949
1279
  for (const [name, value] of Object.entries(catalog)) {
950
1280
  const node = document.getIn([...prefix, name], true);
951
1281
  if (!(0, yaml.isNode)(node) || node.range === void 0 || node.range === null) continue;
@@ -964,7 +1294,7 @@ const checkPackageFiles = ({ files, policy }) => {
964
1294
  });
965
1295
  }
966
1296
  }
967
- const catalogSources = (json) => [json, ...record$2(json["workspaces"]) ? [json["workspaces"]] : []];
1297
+ const catalogSources = (json) => [json, ...record$3(json["workspaces"]) ? [json["workspaces"]] : []];
968
1298
  const pnpmWorkspaceFor = (file) => {
969
1299
  let directory = node_path.default.posix.dirname(file);
970
1300
  while (true) {
@@ -987,8 +1317,8 @@ const checkPackageFiles = ({ files, policy }) => {
987
1317
  patterns: workspace["packages"],
988
1318
  rootMembership: "implicit"
989
1319
  })) return void 0;
990
- if (name === "" || name === "default") return workspace["catalog"] ?? (record$2(workspace["catalogs"]) ? workspace["catalogs"]["default"] : void 0);
991
- return record$2(workspace["catalogs"]) ? workspace["catalogs"][name] : void 0;
1320
+ if (name === "" || name === "default") return workspace["catalog"] ?? (record$3(workspace["catalogs"]) ? workspace["catalogs"]["default"] : void 0);
1321
+ return record$3(workspace["catalogs"]) ? workspace["catalogs"][name] : void 0;
992
1322
  }
993
1323
  let directory = node_path.default.posix.dirname(file);
994
1324
  while (true) {
@@ -996,7 +1326,7 @@ const checkPackageFiles = ({ files, policy }) => {
996
1326
  if (manifest !== void 0) {
997
1327
  for (const source of catalogSources(manifest)) {
998
1328
  if (name === "" && source["catalog"] !== void 0) return source["catalog"];
999
- if (record$2(source["catalogs"]) && source["catalogs"][name] !== void 0) return source["catalogs"][name];
1329
+ if (record$3(source["catalogs"]) && source["catalogs"][name] !== void 0) return source["catalogs"][name];
1000
1330
  }
1001
1331
  if (manifest["workspaces"] !== void 0) return void 0;
1002
1332
  }
@@ -1010,7 +1340,7 @@ const checkPackageFiles = ({ files, policy }) => {
1010
1340
  if (visited.has(value)) return void 0;
1011
1341
  visited.add(value);
1012
1342
  const catalog = catalogFor(file, value.slice(8));
1013
- value = record$2(catalog) ? catalog[name] : void 0;
1343
+ value = record$3(catalog) ? catalog[name] : void 0;
1014
1344
  }
1015
1345
  return value;
1016
1346
  };
@@ -1026,7 +1356,7 @@ const checkPackageFiles = ({ files, policy }) => {
1026
1356
  const workspaces = manifests.get(node_path.default.posix.join(directory, "package.json"))?.["workspaces"];
1027
1357
  if (workspaces !== void 0) return {
1028
1358
  directory,
1029
- patterns: record$2(workspaces) ? workspaces["packages"] : workspaces,
1359
+ patterns: record$3(workspaces) ? workspaces["packages"] : workspaces,
1030
1360
  source: "json"
1031
1361
  };
1032
1362
  if (directory === ".") return void 0;
@@ -1082,7 +1412,7 @@ const checkPackageFiles = ({ files, policy }) => {
1082
1412
  "peerDependencies"
1083
1413
  ]) {
1084
1414
  const dependencies = json[section];
1085
- if (!record$2(dependencies)) continue;
1415
+ if (!record$3(dependencies)) continue;
1086
1416
  for (const [name, value] of Object.entries(dependencies)) {
1087
1417
  if (!tsSpecifiers.has(name)) continue;
1088
1418
  const resolved = resolve(file, name, value);
@@ -1144,7 +1474,7 @@ const checkPackageFiles = ({ files, policy }) => {
1144
1474
  message: `packageManager Bun version must be ${policy.bun}, found ${JSON.stringify(manager)}`
1145
1475
  });
1146
1476
  const checkEntries = (entries, location) => {
1147
- if (!record$2(entries)) return;
1477
+ if (!record$3(entries)) return;
1148
1478
  for (const [name, raw] of Object.entries(entries)) {
1149
1479
  const value = resolve(file, name, raw);
1150
1480
  const tsAllowed = tsSpecifiers.get(name);
@@ -1181,12 +1511,12 @@ const checkPackageFiles = ({ files, policy }) => {
1181
1511
  "devDependencies",
1182
1512
  "optionalDependencies"
1183
1513
  ]) checkEntries(json[key]);
1184
- if (record$2(json["peerDependencies"])) {
1514
+ if (record$3(json["peerDependencies"])) {
1185
1515
  for (const [name, value] of Object.entries(json["peerDependencies"])) if (tsSpecifiers.has(name) && !peerRange(resolve(file, name, value))) checkEntries({ [name]: value });
1186
1516
  }
1187
1517
  for (const source of catalogSources(json)) {
1188
1518
  checkEntries(source["catalog"]);
1189
- if (!record$2(source["catalogs"])) continue;
1519
+ if (!record$3(source["catalogs"])) continue;
1190
1520
  for (const catalog of Object.values(source["catalogs"])) checkEntries(catalog);
1191
1521
  }
1192
1522
  const ownedNames = /* @__PURE__ */ new Set([...pins.keys(), ...tsSpecifiers.keys()]);
@@ -1198,7 +1528,7 @@ const checkPackageFiles = ({ files, policy }) => {
1198
1528
  "optionalDependencies"
1199
1529
  ]) {
1200
1530
  const entries = resolutionRoot[section];
1201
- if (!record$2(entries)) continue;
1531
+ if (!record$3(entries)) continue;
1202
1532
  for (const [name, value] of Object.entries(entries)) resolutionDependencies[name] = resolve(file, name, value);
1203
1533
  }
1204
1534
  const affectedSpecifiers = /* @__PURE__ */ new Map();
@@ -1218,7 +1548,7 @@ const checkPackageFiles = ({ files, policy }) => {
1218
1548
  "optionalDependencies"
1219
1549
  ]) {
1220
1550
  const entries = member[section];
1221
- if (!record$2(entries)) continue;
1551
+ if (!record$3(entries)) continue;
1222
1552
  for (const [name, value] of Object.entries(entries)) {
1223
1553
  if (!tsSpecifiers.has(name)) continue;
1224
1554
  const values = affectedSpecifiers.get(name) ?? /* @__PURE__ */ new Set();
@@ -1228,7 +1558,7 @@ const checkPackageFiles = ({ files, policy }) => {
1228
1558
  }
1229
1559
  }
1230
1560
  const checkResolutionMap = (entries, parent) => {
1231
- if (!record$2(entries)) return;
1561
+ if (!record$3(entries)) return;
1232
1562
  for (const [selector, replacement] of Object.entries(entries)) {
1233
1563
  const target = selector === "." ? parent : selector;
1234
1564
  if (target === void 0) continue;
@@ -1251,7 +1581,7 @@ const checkPackageFiles = ({ files, policy }) => {
1251
1581
  });
1252
1582
  continue;
1253
1583
  }
1254
- if (record$2(replacement)) {
1584
+ if (record$3(replacement)) {
1255
1585
  checkResolutionMap(replacement, target);
1256
1586
  continue;
1257
1587
  }
@@ -1285,7 +1615,7 @@ const checkPackageFiles = ({ files, policy }) => {
1285
1615
  };
1286
1616
  checkResolutionMap(json["overrides"]);
1287
1617
  checkResolutionMap(json["resolutions"]);
1288
- if (record$2(json["pnpm"])) checkResolutionMap(json["pnpm"]["overrides"]);
1618
+ if (record$3(json["pnpm"])) checkResolutionMap(json["pnpm"]["overrides"]);
1289
1619
  const localEntries = toolchainEntries(file, json);
1290
1620
  const usesCompiler = policy.typescriptInstallLayouts.some((layout) => localEntries.some(({ name }) => name === layout.compilerPackage));
1291
1621
  let selectedLayout = layoutFor(specifierMap(localEntries));
@@ -1309,7 +1639,7 @@ const checkPackageFiles = ({ files, policy }) => {
1309
1639
  key: "dependencies",
1310
1640
  message: "TypeScript compiler and compatibility packages must match one complete declared install layout"
1311
1641
  });
1312
- const command = record$2(json["scripts"]) ? json["scripts"]["typecheck"] : void 0;
1642
+ const command = record$3(json["scripts"]) ? json["scripts"]["typecheck"] : void 0;
1313
1643
  if (selectedLayout !== void 0 && typeof command === "string") {
1314
1644
  const normalizedExpected = selectedLayout.typecheckCommand.replace(/\s+/g, " ").trim();
1315
1645
  for (const words of compilerCommandSegments(command)) {
@@ -1327,7 +1657,7 @@ const checkPackageFiles = ({ files, policy }) => {
1327
1657
  }
1328
1658
  }
1329
1659
  }
1330
- if (record$2(json["peerDependencies"])) {
1660
+ if (record$3(json["peerDependencies"])) {
1331
1661
  const peerLayout = selectedLayout ?? workspaceLayoutFor(file)?.layout;
1332
1662
  for (const [name, raw] of Object.entries(json["peerDependencies"])) {
1333
1663
  if (!tsSpecifiers.has(name)) continue;
@@ -1351,7 +1681,7 @@ const checkPackageFiles = ({ files, policy }) => {
1351
1681
  });
1352
1682
  }
1353
1683
  }
1354
- if (record$2(json["engines"]) && json["engines"]["node"] !== void 0 && (typeof json["engines"]["node"] !== "string" || !require_toolchain_inputs.nodeSupportRangeMatches(policy.node, json["engines"]["node"]))) add({
1684
+ if (record$3(json["engines"]) && json["engines"]["node"] !== void 0 && (typeof json["engines"]["node"] !== "string" || !require_toolchain_inputs.nodeSupportRangeMatches(policy.node, json["engines"]["node"]))) add({
1355
1685
  file,
1356
1686
  rule: "node-engine",
1357
1687
  key: "node",
@@ -1362,6 +1692,232 @@ const checkPackageFiles = ({ files, policy }) => {
1362
1692
  return diagnostics;
1363
1693
  };
1364
1694
  //#endregion
1695
+ //#region src/toolchain-rust.ts
1696
+ const record$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1697
+ /** Exceptions authorize one immutable release, never a channel or an entire rule. */
1698
+ const parseRustExceptions = (input, today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)) => {
1699
+ if (input === void 0) return [];
1700
+ if (!Array.isArray(input)) throw new Error("rustExceptions must be an array");
1701
+ const pins = /* @__PURE__ */ new Set();
1702
+ return input.map((entry) => {
1703
+ if (!record$2(entry) || Object.keys(entry).length !== 4 || Object.keys(entry).some((key) => ![
1704
+ "pin",
1705
+ "reason",
1706
+ "tracking",
1707
+ "expires"
1708
+ ].includes(key)) || typeof entry["pin"] !== "string" || !/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/.test(entry["pin"]) || typeof entry["reason"] !== "string" || entry["reason"].trim() === "" || typeof entry["tracking"] !== "string" || !/^https:\/\/github\.com\/[^/\s]+\/[^/\s]+\/issues\/[1-9]\d*$/.test(entry["tracking"]) || typeof entry["expires"] !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(entry["expires"]) || !Number.isFinite(Date.parse(entry["expires"])) || new Date(entry["expires"]).toISOString().slice(0, 10) !== entry["expires"] || entry["expires"] <= today || pins.has(entry["pin"])) throw new Error("each Rust exception requires a unique exact stable pin, reason, GitHub issue and future valid expiry date");
1709
+ pins.add(entry["pin"]);
1710
+ return {
1711
+ pin: entry["pin"],
1712
+ reason: entry["reason"],
1713
+ tracking: entry["tracking"],
1714
+ expires: entry["expires"]
1715
+ };
1716
+ });
1717
+ };
1718
+ /** Only executable fields are inspected: unrelated action inputs are not Rust selectors. */
1719
+ const checkRustReferences = (options) => {
1720
+ const { file, rust, exceptions } = options;
1721
+ const text = options.type === "file" ? options.text : "";
1722
+ const diagnostics = [];
1723
+ const select = (value, label, line = 1) => {
1724
+ if (value === rust || exceptions.some(({ pin }) => pin === value)) return;
1725
+ diagnostics.push({
1726
+ rule: "rust-version",
1727
+ path: file,
1728
+ line,
1729
+ message: `${label} must be Rust ${rust}, found ${String(value)}`
1730
+ });
1731
+ };
1732
+ const shell = (source, firstLine = 1) => {
1733
+ const commands = shellCommandSegments(source).map((words) => words.map((word) => ({
1734
+ value: decodeShellWord(word),
1735
+ line: firstLine
1736
+ })));
1737
+ for (const command of commands) {
1738
+ let position = 0;
1739
+ const prefixes = /* @__PURE__ */ new Set([
1740
+ "if",
1741
+ "then",
1742
+ "do",
1743
+ "elif",
1744
+ "!",
1745
+ "env",
1746
+ "sudo",
1747
+ "command",
1748
+ "exec",
1749
+ "time",
1750
+ "export",
1751
+ "readonly",
1752
+ "/usr/bin/env",
1753
+ "/bin/env"
1754
+ ]);
1755
+ while (position < command.length) {
1756
+ const value = command.at(position)?.value;
1757
+ if (value?.startsWith("RUSTUP_TOOLCHAIN=")) select(value.slice(17), "RUSTUP_TOOLCHAIN", firstLine);
1758
+ if (value === "-u" || value === "--unset") {
1759
+ position += 2;
1760
+ continue;
1761
+ }
1762
+ if (value?.startsWith("--unset=") || value === "-i" || value === "--ignore-environment") {
1763
+ position++;
1764
+ continue;
1765
+ }
1766
+ if (value !== void 0 && (prefixes.has(value) || /^[A-Za-z_][A-Za-z_\d]*=/.test(value))) position++;
1767
+ else break;
1768
+ }
1769
+ const executable = command.at(position)?.value;
1770
+ const arguments_ = command.slice(position + 1);
1771
+ if (executable === "cargo" && arguments_.at(0)?.value.startsWith("+")) {
1772
+ const selector = arguments_.at(0);
1773
+ select(selector?.value.slice(1), "cargo toolchain", selector?.line);
1774
+ }
1775
+ if (executable !== "rustup") continue;
1776
+ let subcommandPosition = 0;
1777
+ while (subcommandPosition < arguments_.length) {
1778
+ const token = arguments_.at(subcommandPosition);
1779
+ if (token === void 0) break;
1780
+ if (token.value === "--") {
1781
+ subcommandPosition++;
1782
+ break;
1783
+ }
1784
+ if (token.value.startsWith("+")) select(token.value.slice(1), "rustup toolchain", token.line);
1785
+ else if (!token.value.startsWith("-")) break;
1786
+ subcommandPosition++;
1787
+ }
1788
+ const invocation = arguments_.slice(subcommandPosition);
1789
+ const subcommand = invocation.at(0)?.value;
1790
+ if (subcommand === "target" || subcommand === "component") {
1791
+ for (let index = 1; index < invocation.length; index++) {
1792
+ const token = invocation.at(index);
1793
+ if (token?.value === "--toolchain") select(invocation.at(++index)?.value, "rustup --toolchain", token.line);
1794
+ else if (token?.value.startsWith("--toolchain=")) select(token.value.slice(12), "rustup --toolchain", token.line);
1795
+ }
1796
+ continue;
1797
+ }
1798
+ let selectors;
1799
+ let requiresSelector = true;
1800
+ let firstSelectorOnly = false;
1801
+ if (subcommand === "default" || subcommand === "install" || subcommand === "add" || subcommand === "update") {
1802
+ selectors = invocation.slice(1);
1803
+ requiresSelector = subcommand !== "update";
1804
+ } else if (subcommand === "toolchain" && [
1805
+ "install",
1806
+ "add",
1807
+ "update"
1808
+ ].includes(invocation.at(1)?.value ?? "")) {
1809
+ selectors = invocation.slice(2);
1810
+ requiresSelector = invocation.at(1)?.value !== "update";
1811
+ } else if (subcommand === "run") {
1812
+ selectors = invocation.slice(1);
1813
+ firstSelectorOnly = true;
1814
+ } else if (subcommand === "override" && invocation.at(1)?.value === "set") {
1815
+ selectors = invocation.slice(2);
1816
+ firstSelectorOnly = true;
1817
+ } else continue;
1818
+ let selected = 0;
1819
+ const optionArguments = /* @__PURE__ */ new Set([
1820
+ "--path",
1821
+ "--profile",
1822
+ "--component",
1823
+ "-c",
1824
+ "--target",
1825
+ "-t"
1826
+ ]);
1827
+ for (let index = 0; index < selectors.length; index++) {
1828
+ const token = selectors.at(index);
1829
+ if (token === void 0) continue;
1830
+ if (optionArguments.has(token.value)) {
1831
+ index++;
1832
+ continue;
1833
+ }
1834
+ if (token.value.startsWith("-")) continue;
1835
+ selected++;
1836
+ select(token.value, "rustup toolchain", token.line);
1837
+ if (firstSelectorOnly) break;
1838
+ }
1839
+ if (selected === 0 && requiresSelector) select(void 0, "rustup toolchain", firstLine);
1840
+ }
1841
+ };
1842
+ if (options.type === "environment") {
1843
+ for (const { key, value } of options.assignments) if (key === "RUSTUP_TOOLCHAIN") select(value, key, options.line);
1844
+ return diagnostics;
1845
+ }
1846
+ if (options.type === "command") {
1847
+ shell(options.source, options.line);
1848
+ return diagnostics;
1849
+ }
1850
+ const lineOf = (token) => {
1851
+ const index = text.split(/\r?\n/).findIndex((line) => line.includes(token));
1852
+ return index < 0 ? 1 : index + 1;
1853
+ };
1854
+ if (require_toolchain_inputs.isShellScript(file, text)) shell(text);
1855
+ if (/(?:^|\/)package\.json$/.test(file)) try {
1856
+ const object = JSON.parse(text);
1857
+ if (record$2(object) && record$2(object["scripts"])) {
1858
+ for (const [name, script] of Object.entries(object["scripts"])) if (typeof script === "string") shell(script, lineOf(`"${name}"`));
1859
+ }
1860
+ } catch {}
1861
+ if (/(?:^|\/)ci-service-images\.json$/.test(file)) {
1862
+ const images = (value) => {
1863
+ if (typeof value === "string" && /^rust(?=:|@|$)/.test(canonicalDockerRuntime(value))) {
1864
+ const tag = canonicalDockerRuntime(value).replace(/^rust:?/, "").split("@").at(0);
1865
+ const version = tag === void 0 ? void 0 : rustImageVersion(tag);
1866
+ select(version, "Rust service image", lineOf(value));
1867
+ } else if (Array.isArray(value)) value.forEach(images);
1868
+ else if (record$2(value)) Object.values(value).forEach(images);
1869
+ };
1870
+ try {
1871
+ images(JSON.parse(text));
1872
+ } catch {
1873
+ diagnostics.push({
1874
+ rule: "rust-version",
1875
+ path: file,
1876
+ line: 1,
1877
+ message: "invalid service image JSON"
1878
+ });
1879
+ }
1880
+ }
1881
+ if (require_toolchain_inputs.githubAutomationFileKind(file) === void 0) return diagnostics;
1882
+ const document = (0, yaml.parseDocument)(text);
1883
+ if (document.errors.length > 0) return diagnostics;
1884
+ try {
1885
+ const visit = (value) => {
1886
+ if (Array.isArray(value)) {
1887
+ value.forEach(visit);
1888
+ return;
1889
+ }
1890
+ if (!record$2(value)) return;
1891
+ if (record$2(value["env"]) && value["env"]["RUSTUP_TOOLCHAIN"] !== void 0) select(value["env"]["RUSTUP_TOOLCHAIN"], "RUSTUP_TOOLCHAIN", lineOf("RUSTUP_TOOLCHAIN"));
1892
+ if (typeof value["run"] === "string") shell(value["run"], lineOf("run:"));
1893
+ const uses = value["uses"];
1894
+ if (typeof uses === "string") {
1895
+ const action = uses.split("@").at(0)?.toLowerCase();
1896
+ const inputs = record$2(value["with"]) ? value["with"] : {};
1897
+ if (action === "dtolnay/rust-toolchain" || action === "actions-rust-lang/setup-rust-toolchain") {
1898
+ const entries = Object.entries(inputs).filter(([key]) => key.toLowerCase() === "toolchain");
1899
+ if (entries.length !== 1) select(void 0, `${action} requires an explicit exact toolchain input`, lineOf(uses));
1900
+ else {
1901
+ const selected = entries.at(0)?.at(1);
1902
+ const selectors = action === "actions-rust-lang/setup-rust-toolchain" && typeof selected === "string" ? selected.split(",").map((pin) => pin.trim()) : [selected];
1903
+ for (const selector of selectors) select(selector, `${action} toolchain`, lineOf("toolchain:"));
1904
+ }
1905
+ } else if (/(?:^|\/)\.github\/workflows\/[^@]+\.ya?ml(?:@|$)/.test(uses)) {
1906
+ for (const [key, selected] of Object.entries(inputs)) if (["toolchain", "rust-toolchain"].includes(key.toLowerCase())) select(selected, `reusable workflow ${key}`, lineOf(`${key}:`));
1907
+ }
1908
+ }
1909
+ for (const [key, child] of Object.entries(value)) {
1910
+ if (key === "workflow_call" && record$2(child) && record$2(child["inputs"])) {
1911
+ for (const [input, declaration] of Object.entries(child["inputs"])) if (["toolchain", "rust-toolchain"].includes(input) && record$2(declaration) && declaration["default"] !== void 0) select(declaration["default"], `reusable workflow default ${input}`, lineOf("default:"));
1912
+ }
1913
+ visit(child);
1914
+ }
1915
+ };
1916
+ visit(document.toJS({ maxAliasCount: 100 }));
1917
+ } catch {}
1918
+ return diagnostics;
1919
+ };
1920
+ //#endregion
1365
1921
  //#region src/toolchain-runtime.ts
1366
1922
  const runtimeRules = [
1367
1923
  "bun-pins",
@@ -1374,6 +1930,69 @@ const runtimeRules = [
1374
1930
  "action-pins"
1375
1931
  ];
1376
1932
  const record$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1933
+ const dockerEnvironmentAssignments = ({ source, escape }) => {
1934
+ const words = [];
1935
+ let word = "";
1936
+ let quote;
1937
+ for (let index = 0; index < source.length; index++) {
1938
+ const character = source.at(index) ?? "";
1939
+ if (character === escape && quote !== "'") {
1940
+ word += character + (source.at(++index) ?? "");
1941
+ continue;
1942
+ }
1943
+ if (character === quote) quote = void 0;
1944
+ else if (quote === void 0 && (character === "'" || character === "\"")) quote = character;
1945
+ else if (quote === void 0 && /\s/.test(character)) {
1946
+ if (word !== "") words.push(word);
1947
+ word = "";
1948
+ continue;
1949
+ }
1950
+ word += character;
1951
+ }
1952
+ if (quote !== void 0) return void 0;
1953
+ if (word !== "") words.push(word);
1954
+ const decode = (raw) => {
1955
+ let value = "";
1956
+ let quoted;
1957
+ for (let index = 0; index < raw.length; index++) {
1958
+ const character = raw.at(index) ?? "";
1959
+ if (character === quoted) {
1960
+ quoted = void 0;
1961
+ continue;
1962
+ }
1963
+ if (quoted === void 0 && (character === "'" || character === "\"")) {
1964
+ quoted = character;
1965
+ continue;
1966
+ }
1967
+ const next = raw.at(index + 1);
1968
+ if (character === escape && quoted !== "'" && next !== void 0 && (quoted === void 0 || next === "\"" || next === "$" || next === escape)) {
1969
+ value += next;
1970
+ index++;
1971
+ } else value += character;
1972
+ }
1973
+ return value;
1974
+ };
1975
+ const first = words.at(0);
1976
+ if (first === void 0) return void 0;
1977
+ if (!first.includes("=")) {
1978
+ const legacy = /^(\S+)\s+([\s\S]*)$/.exec(source);
1979
+ if (legacy === null) return void 0;
1980
+ return [{
1981
+ key: decode(legacy[1] ?? ""),
1982
+ value: decode(legacy[2] ?? "")
1983
+ }];
1984
+ }
1985
+ const assignments = [];
1986
+ for (const raw of words) {
1987
+ const separator = raw.indexOf("=");
1988
+ if (separator < 1) return void 0;
1989
+ assignments.push({
1990
+ key: decode(raw.slice(0, separator)),
1991
+ value: decode(raw.slice(separator + 1))
1992
+ });
1993
+ }
1994
+ return assignments;
1995
+ };
1377
1996
  const pythonSelectorMatches = (value, expected) => value === expected || typeof value === "string" && /^\d+\.\d+$/.test(expected) && value.startsWith(`${expected}.`) && /^\d+\.\d+\.\d+$/.test(value);
1378
1997
  const compareRelease = (left, right) => {
1379
1998
  for (let index = 0; index < Math.max(left.length, right.length); index++) {
@@ -1443,8 +2062,14 @@ const pythonRequirementMatches = (requirement, version) => {
1443
2062
  });
1444
2063
  };
1445
2064
  /** Check runtime declarations only in the files that declare or configure them. */
1446
- const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, repository, onDelegated, dynamicSelectors = [], onDynamicSelector, engineFloors = [], onEngineFloor }) => {
1447
- const diagnostics = [];
2065
+ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, repository, onDelegated, dynamicSelectors = [], onDynamicSelector, rustExceptions = [], engineFloors = [], onEngineFloor }) => {
2066
+ const diagnostics = checkRustReferences({
2067
+ type: "file",
2068
+ file,
2069
+ text,
2070
+ rust: policy.rust,
2071
+ exceptions: rustExceptions
2072
+ });
1448
2073
  const name = node_path.default.posix.basename(file);
1449
2074
  const lines = text.split(/\r?\n/);
1450
2075
  const add = ({ rule, line, message }) => {
@@ -1467,7 +2092,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
1467
2092
  };
1468
2093
  const lineOf = (key) => Math.max(1, lines.findIndex((line) => line.includes(key)) + 1);
1469
2094
  const pin = ({ rule, value, expected, label, line = 1, selector }) => {
1470
- let matches = value === expected;
2095
+ let matches = value === expected || rule === "rust-version" && rustExceptions.some((entry) => entry.pin === value);
1471
2096
  if (selector === "node") matches = require_toolchain_inputs.nodeSelectorMatches(value, expected);
1472
2097
  else if (label.includes("python")) matches = pythonSelectorMatches(value, expected);
1473
2098
  if (!matches) add({
@@ -1537,12 +2162,10 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
1537
2162
  const parsed = toml("rust-version");
1538
2163
  if (parsed !== void 0) {
1539
2164
  const toolchain = parsed["toolchain"];
1540
- const components = record$1(toolchain) ? toolchain["components"] : void 0;
1541
- const expected = Array.isArray(components) && components.includes("rustc-dev") ? policy.rustCompilerDevelopment : policy.rust;
1542
2165
  pin({
1543
2166
  rule: "rust-version",
1544
2167
  value: record$1(toolchain) ? toolchain["channel"] : void 0,
1545
- expected,
2168
+ expected: policy.rust,
1546
2169
  label: "Rust channel",
1547
2170
  line: lineOf("channel")
1548
2171
  });
@@ -1643,7 +2266,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
1643
2266
  if (record$1(tools)) for (const [tool, expected] of Object.entries(versions)) {
1644
2267
  const configured = tools[tool];
1645
2268
  if (configured !== void 0) pin({
1646
- rule: "runtime-manager",
2269
+ rule: tool === "rust" || tool === "core:rust" ? "rust-version" : "runtime-manager",
1647
2270
  value: record$1(configured) ? configured["version"] : configured,
1648
2271
  expected,
1649
2272
  selector: nodeTools.has(tool) ? "node" : void 0,
@@ -1658,7 +2281,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
1658
2281
  if (tool === void 0 || !(tool in versions)) return;
1659
2282
  const expected = Object.entries(versions).find(([key]) => key === tool)?.[1];
1660
2283
  if (expected !== void 0) pin({
1661
- rule: "runtime-manager",
2284
+ rule: tool === "rust" || tool === "core:rust" ? "rust-version" : "runtime-manager",
1662
2285
  value: fields.slice(1).join(" "),
1663
2286
  expected,
1664
2287
  selector: nodeTools.has(tool) ? "node" : void 0,
@@ -1668,11 +2291,12 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
1668
2291
  });
1669
2292
  const checkRuntimeImage = ({ image, line, label, at }) => {
1670
2293
  const unknownRuntime = () => {
1671
- if (dynamic(image) && authorizeDynamic("image", line, at)) return;
2294
+ const rustImage = typeof image === "string" && /^rust(?=:|@|$)/i.test(canonicalDockerRuntime(image));
2295
+ if (!rustImage && dynamic(image) && authorizeDynamic("image", line, at)) return;
1672
2296
  add({
1673
- rule: "runtime-docker",
2297
+ rule: rustImage ? "rust-version" : "runtime-docker",
1674
2298
  line,
1675
- message: `cannot determine ${label} runtime; use a static image or a scoped dynamicSelectors declaration`
2299
+ message: rustImage ? `cannot determine ${label} Rust release; use a static exact pin` : `cannot determine ${label} runtime; use a static image or a scoped dynamicSelectors declaration`
1676
2300
  });
1677
2301
  };
1678
2302
  if (typeof image !== "string") {
@@ -1684,7 +2308,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
1684
2308
  const digestSeparator = structuralImage.indexOf("@");
1685
2309
  const reference = digestSeparator < 0 ? canonical : canonical.slice(0, digestSeparator);
1686
2310
  const structuralReference = digestSeparator < 0 ? structuralImage : structuralImage.slice(0, digestSeparator);
1687
- const managedFamily = /^(node|python|oven\/bun)(?=:|$)/i.exec(structuralReference)?.[1];
2311
+ const managedFamily = /^(node|python|rust|oven\/bun)(?=:|$)/i.exec(structuralReference)?.[1];
1688
2312
  const tagSeparator = managedFamily === void 0 ? structuralReference.lastIndexOf(":") : structuralReference.indexOf(":", managedFamily.length);
1689
2313
  const hasTag = managedFamily === void 0 ? tagSeparator > structuralReference.lastIndexOf("/") : tagSeparator !== -1;
1690
2314
  const imageRepository = hasTag ? reference.slice(0, tagSeparator) : reference;
@@ -1692,8 +2316,11 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
1692
2316
  unknownRuntime();
1693
2317
  return;
1694
2318
  }
1695
- if (!/^(node|python|oven\/bun)$/i.test(imageRepository)) return;
2319
+ if (!/^(node|python|rust|oven\/bun)$/i.test(imageRepository)) return;
1696
2320
  const tool = imageRepository.toLowerCase();
2321
+ let imageRule = "runtime-docker";
2322
+ if (tool === "rust") imageRule = "rust-version";
2323
+ else if (tool === "oven/bun") imageRule = "bun-pins";
1697
2324
  if (image.includes("$")) {
1698
2325
  unknownRuntime();
1699
2326
  return;
@@ -1701,7 +2328,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
1701
2328
  const digest = digestSeparator < 0 ? void 0 : canonical.slice(digestSeparator + 1);
1702
2329
  if (digest !== void 0 && !/^sha256:[a-fA-F\d]{64}$/.test(digest)) {
1703
2330
  add({
1704
- rule: tool === "oven/bun" ? "bun-pins" : "runtime-docker",
2331
+ rule: imageRule,
1705
2332
  line,
1706
2333
  message: `${label} ${tool} digest must be sha256 followed by 64 hexadecimal characters`
1707
2334
  });
@@ -1709,14 +2336,15 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
1709
2336
  }
1710
2337
  const tag = hasTag ? reference.slice(tagSeparator + 1) : void 0;
1711
2338
  const variantSuffix = tool === "oven/bun" ? /-(?:alpine|slim|debian)$/ : /-(?:alpine(?:\d+(?:\.\d+)*)?|(?:bookworm|bullseye|trixie|buster)(?:-slim)?|slim)$/;
1712
- const version = tag?.replace(variantSuffix, "");
2339
+ const version = tool === "rust" && tag !== void 0 ? rustImageVersion(tag) : tag?.replace(variantSuffix, "");
1713
2340
  let expected = policy.bun;
1714
2341
  if (tool === "node") expected = policy.node;
1715
2342
  else if (tool === "python") expected = policy.python;
2343
+ else if (tool === "rust") expected = policy.rust;
1716
2344
  if (digest !== void 0) {
1717
2345
  if (version === void 0 || !/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/.test(version)) {
1718
2346
  add({
1719
- rule: tool === "oven/bun" ? "bun-pins" : "runtime-docker",
2347
+ rule: imageRule,
1720
2348
  line,
1721
2349
  message: `${label} ${tool} digest requires an exact stable patch tag matching ${expected}`
1722
2350
  });
@@ -1734,7 +2362,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
1734
2362
  }
1735
2363
  }
1736
2364
  pin({
1737
- rule: tool === "oven/bun" ? "bun-pins" : "runtime-docker",
2365
+ rule: imageRule,
1738
2366
  value: version,
1739
2367
  expected,
1740
2368
  selector: tool === "node" ? "node" : void 0,
@@ -1751,6 +2379,17 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
1751
2379
  const heredocs = [];
1752
2380
  let instruction = "";
1753
2381
  let startLine = 1;
2382
+ let pendingRustRun;
2383
+ const checkRustCommand = (source, line) => {
2384
+ diagnostics.push(...checkRustReferences({
2385
+ type: "command",
2386
+ file,
2387
+ source,
2388
+ line,
2389
+ rust: policy.rust,
2390
+ exceptions: rustExceptions
2391
+ }));
2392
+ };
1754
2393
  const expand = (value) => {
1755
2394
  for (let count = 0; count <= variables.size; count++) {
1756
2395
  const next = value.replace(/\$(?:\{([A-Za-z_][A-Za-z_\d]*)\}|([A-Za-z_][A-Za-z_\d]*))/g, (reference, braced, plain) => variables.get(braced ?? plain ?? "") ?? reference);
@@ -1837,7 +2476,49 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
1837
2476
  if (key !== void 0 && value !== void 0) variables.set(key, expand(value.replace(/^(["'])(.*)\1$/, "$2")));
1838
2477
  return;
1839
2478
  }
1840
- if (/^\s*(?:RUN|COPY)\s+/i.test(content)) {
2479
+ const run = /^\s*RUN\s+(.+)$/i.exec(content);
2480
+ if (run !== null) {
2481
+ let source = run[1] ?? "";
2482
+ source = source.replace(/^(?:--\S+\s+)*/, "");
2483
+ if (source.startsWith("[")) try {
2484
+ const words = JSON.parse(source);
2485
+ if (Array.isArray(words) && words.every((word) => typeof word === "string")) source = words.map((word) => `'${word.replaceAll("'", "'\\''")}'`).join(" ");
2486
+ } catch {}
2487
+ const implicit = /^(<<-?(?:'[^']+'|"[^"]+"|\S+))(?:\s+(.+))?$/.exec(source);
2488
+ if (implicit !== null && implicit[2] === void 0) source = `sh ${source}`;
2489
+ collectHeredocs(content, line);
2490
+ if (heredocs.length > 0) pendingRustRun = {
2491
+ source,
2492
+ line
2493
+ };
2494
+ else checkRustCommand(source, line);
2495
+ return;
2496
+ }
2497
+ const environment = /^\s*ENV\s+(.+)$/i.exec(content);
2498
+ if (environment !== null) {
2499
+ const assignments = dockerEnvironmentAssignments({
2500
+ source: environment[1] ?? "",
2501
+ escape
2502
+ });
2503
+ if (assignments === void 0) {
2504
+ add({
2505
+ rule: "runtime-docker",
2506
+ line,
2507
+ message: "invalid Docker ENV assignments"
2508
+ });
2509
+ return;
2510
+ }
2511
+ diagnostics.push(...checkRustReferences({
2512
+ type: "environment",
2513
+ file,
2514
+ assignments,
2515
+ line,
2516
+ rust: policy.rust,
2517
+ exceptions: rustExceptions
2518
+ }));
2519
+ return;
2520
+ }
2521
+ if (/^\s*COPY\s+/i.test(content)) {
1841
2522
  collectHeredocs(content, line);
1842
2523
  return;
1843
2524
  }
@@ -1858,7 +2539,12 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
1858
2539
  lines.forEach((line, index) => {
1859
2540
  const heredoc = heredocs.at(0);
1860
2541
  if (heredoc !== void 0) {
2542
+ if (pendingRustRun !== void 0) pendingRustRun.source += `\n${line}`;
1861
2543
  if ((heredoc.stripTabs ? line.replace(/^\t+/, "") : line) === heredoc.delimiter) heredocs.shift();
2544
+ if (heredocs.length === 0 && pendingRustRun !== void 0) {
2545
+ checkRustCommand(pendingRustRun.source, pendingRustRun.line);
2546
+ pendingRustRun = void 0;
2547
+ }
1862
2548
  return;
1863
2549
  }
1864
2550
  if (line.trimStart().startsWith("#")) return;
@@ -2533,18 +3219,20 @@ const record = (value) => typeof value === "object" && value !== null && !Array.
2533
3219
  const array = (value) => Array.isArray(value);
2534
3220
  /** Repository-owned declarations are explicit; malformed decisions fail closed. */
2535
3221
  const parseToolchainConfiguration = (input) => {
2536
- if (!record(input) || Object.keys(input).some((key) => key !== "optOuts" && key !== "cloud" && key !== "engineFloors" && key !== "dynamicSelectors" && key !== "consumerChecks") || input["optOuts"] !== void 0 && !array(input["optOuts"])) throw new Error("stll-toolchain.json accepts only optOuts, cloud, engineFloors, dynamicSelectors and consumerChecks declarations");
3222
+ if (!record(input) || Object.keys(input).some((key) => key !== "optOuts" && key !== "cloud" && key !== "engineFloors" && key !== "dynamicSelectors" && key !== "consumerChecks" && key !== "rustExceptions") || input["optOuts"] !== void 0 && !array(input["optOuts"])) throw new Error("stll-toolchain.json accepts only optOuts, cloud, engineFloors, dynamicSelectors consumerChecks and rustExceptions declarations");
2537
3223
  const disabled = /* @__PURE__ */ new Set();
2538
3224
  const optOuts = input["optOuts"] ?? [];
2539
3225
  if (!array(optOuts)) throw new Error("optOuts must be an array");
2540
3226
  for (const entry of optOuts) {
2541
3227
  if (!record(entry) || Object.keys(entry).some((key) => key !== "rule" && key !== "reason") || typeof entry["rule"] !== "string" || !toolchainRules.some((rule) => rule === entry["rule"]) || typeof entry["reason"] !== "string" || entry["reason"].trim() === "" || disabled.has(entry["rule"])) throw new Error("each opt-out requires a unique known rule and a nonempty reason");
3228
+ if (entry["rule"] === "rust-version") throw new Error("rust-version requires expiring exact-pin rustExceptions, not an opt-out");
2542
3229
  if (entry["rule"] === "node-engine") throw new Error("node-engine cannot be opted out; engines.node must support the shared Node series");
2543
3230
  if (entry["rule"] === "cloud-setup-drift") throw new Error("cloud-setup-drift cannot be opted out; generated setup must match shared policy");
2544
3231
  disabled.add(entry["rule"]);
2545
3232
  }
2546
3233
  return {
2547
3234
  disabled,
3235
+ rustExceptions: parseRustExceptions(input["rustExceptions"]),
2548
3236
  cloud: require_toolchain_inputs.parseCloudSetup(input["cloud"]),
2549
3237
  engineFloors: parseEngineFloors(input["engineFloors"]),
2550
3238
  dynamicSelectors: parseDynamicSelectors(input["dynamicSelectors"]),
@@ -2557,27 +3245,47 @@ const readToolchainInputs = (root) => {
2557
3245
  const tracked = (0, node_child_process.execFileSync)("git", [
2558
3246
  "ls-files",
2559
3247
  "-z",
2560
- "--cached"
3248
+ "--cached",
3249
+ "--stage"
2561
3250
  ], {
2562
3251
  cwd: root,
2563
3252
  encoding: "utf8"
2564
- }).split("\0").filter((file) => file !== "");
3253
+ }).split("\0").filter((entry) => entry !== "" && !entry.startsWith("160000 ")).map((entry) => entry.slice(entry.indexOf(" ") + 1));
2565
3254
  const files = {};
2566
3255
  const trackedFiles = new Set(tracked);
2567
3256
  const resolvedRoot = (0, node_fs.realpathSync)(root);
2568
3257
  for (const file of tracked) {
2569
3258
  const kind = require_toolchain_inputs.toolchainInputKind(file);
2570
- if (kind === void 0) continue;
3259
+ const shebangCandidate = kind === void 0 && node_path.default.posix.extname(file) === "" && !file.split("/").some((part) => part === "vendor" || part === "node_modules");
3260
+ if (kind === void 0 && !shebangCandidate) continue;
2571
3261
  if (kind === "presence") {
2572
3262
  files[file] = "";
2573
3263
  continue;
2574
3264
  }
2575
3265
  try {
2576
- const resolvedFile = (0, node_fs.realpathSync)(node_path.default.join(resolvedRoot, file));
3266
+ const inputPath = node_path.default.join(resolvedRoot, file);
3267
+ if (shebangCandidate) {
3268
+ const input = (0, node_fs.lstatSync)(inputPath);
3269
+ if (!input.isFile() && !input.isSymbolicLink()) continue;
3270
+ }
3271
+ const resolvedFile = (0, node_fs.realpathSync)(inputPath);
2577
3272
  const relative = node_path.default.relative(resolvedRoot, resolvedFile);
2578
3273
  if (node_path.default.isAbsolute(relative) || relative.split(node_path.default.sep).at(0) === ".." || !trackedFiles.has(relative.split(node_path.default.sep).join("/"))) throw new Error("Configuration target is outside the tracked tree");
3274
+ if (shebangCandidate) {
3275
+ if (!(0, node_fs.lstatSync)(resolvedFile).isFile()) continue;
3276
+ const descriptor = (0, node_fs.openSync)(resolvedFile, "r");
3277
+ const prefix = Buffer.alloc(512);
3278
+ let count;
3279
+ try {
3280
+ count = (0, node_fs.readSync)(descriptor, prefix, 0, prefix.length, 0);
3281
+ } finally {
3282
+ (0, node_fs.closeSync)(descriptor);
3283
+ }
3284
+ if (require_toolchain_inputs.toolchainInputKind(file, prefix.toString("utf8", 0, count)) === void 0) continue;
3285
+ }
2579
3286
  files[file] = (0, node_fs.readFileSync)(resolvedFile, "utf8");
2580
3287
  } catch {
3288
+ if (shebangCandidate) continue;
2581
3289
  diagnostics.push({
2582
3290
  rule: "configuration",
2583
3291
  path: file,
@@ -2678,6 +3386,7 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
2678
3386
  }
2679
3387
  },
2680
3388
  trackedFiles,
3389
+ rustExceptions: configuration.rustExceptions,
2681
3390
  dynamicSelectors: dynamicSelectors.filter((entry) => entry.path === file),
2682
3391
  onDynamicSelector: (entry) => selectorMatches.set(entry, (selectorMatches.get(entry) ?? 0) + 1),
2683
3392
  repository,