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