@stll/oxlint-config 0.15.0 → 1.0.1

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.15.0";
15
+ const consumerRunnerVersion = "1.0.1";
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,16 +259,24 @@ 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;
269
271
  const sortedExpected = [...expected].sort();
270
272
  return [...value].sort().every((entry, index) => entry === sortedExpected[index]);
271
273
  };
274
+ const pinnedVersionUpdateTypes = [
275
+ "version-update:semver-major",
276
+ "version-update:semver-minor",
277
+ "version-update:semver-patch"
278
+ ];
279
+ const versionOnlyIgnore = (entry) => record$6(entry) && Object.keys(entry).length === 2 && typeof entry["dependency-name"] === "string" && sameStrings(entry["update-types"], pinnedVersionUpdateTypes);
272
280
  const directoryOf = (file) => {
273
281
  const directory = node_path.default.posix.dirname(file);
274
282
  return directory === "." ? "/" : `/${directory}`;
@@ -284,8 +292,8 @@ const workspaceTable = (parsed, ecosystem) => {
284
292
  case "cargo": return parsed["workspace"];
285
293
  case "uv": {
286
294
  const tool = parsed["tool"];
287
- const uv = record$5(tool) ? tool["uv"] : void 0;
288
- return record$5(uv) ? uv["workspace"] : void 0;
295
+ const uv = record$6(tool) ? tool["uv"] : void 0;
296
+ return record$6(uv) ? uv["workspace"] : void 0;
289
297
  }
290
298
  default: throw new Error("unknown workspace ecosystem", { cause: ecosystem });
291
299
  }
@@ -305,7 +313,7 @@ const workspaceRoots = (files, ecosystem) => {
305
313
  const document = (0, yaml.parseDocument)(text);
306
314
  if (document.errors.length > 0) throw new Error(`${workspaceFile}:1: invalid pnpm workspace YAML`);
307
315
  const parsed = document.toJS({ maxAliasCount: 100 });
308
- if (!record$5(parsed)) throw new Error(`${workspaceFile}:1: pnpm workspace must contain an object`);
316
+ if (!record$6(parsed)) throw new Error(`${workspaceFile}:1: pnpm workspace must contain an object`);
309
317
  const patterns = parsed["packages"] ?? [];
310
318
  if (!strings(patterns)) throw new Error(`${workspaceFile}:1: pnpm packages must be a string array`);
311
319
  workspaces.set(directory, patterns.map((pattern) => pattern.startsWith("!") ? `!${node_path.default.posix.normalize(pattern.slice(1))}` : node_path.default.posix.normalize(pattern)));
@@ -317,13 +325,13 @@ const workspaceRoots = (files, ecosystem) => {
317
325
  }
318
326
  try {
319
327
  const parsed = ecosystem === "npm" ? JSON.parse(files[file] ?? "") : (0, smol_toml.parse)(files[file] ?? "");
320
- if (!record$5(parsed)) continue;
328
+ if (!record$6(parsed)) continue;
321
329
  if (ecosystem === "npm" && typeof parsed["packageManager"] === "string" && parsed["packageManager"].startsWith("pnpm@")) continue;
322
330
  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;
331
+ const patterns = record$6(value) ? value[ecosystem === "npm" ? "packages" : "members"] : value;
332
+ const members = ecosystem === "cargo" && record$6(value) && patterns === void 0 ? [] : patterns;
325
333
  if (strings(members)) {
326
- const excluded = ecosystem !== "npm" && record$5(value) && strings(value["exclude"]) ? value["exclude"].map((pattern) => `!${pattern}`) : [];
334
+ const excluded = ecosystem !== "npm" && record$6(value) && strings(value["exclude"]) ? value["exclude"].map((pattern) => `!${pattern}`) : [];
327
335
  workspaces.set(node_path.default.posix.dirname(file), [...members, ...excluded]);
328
336
  }
329
337
  } catch {}
@@ -374,11 +382,11 @@ const workspaceRoots = (files, ecosystem) => {
374
382
  for (const directory of memberDirectories) try {
375
383
  const parsed = (0, smol_toml.parse)(files[node_path.default.posix.join(directory, "Cargo.toml")] ?? "");
376
384
  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;
385
+ const inherited = record$6(workspace) ? workspace["dependencies"] : void 0;
378
386
  const dependencyOwners = [parsed];
379
387
  const targets = parsed["target"];
380
- if (record$5(targets)) {
381
- for (const target of Object.values(targets)) if (record$5(target)) dependencyOwners.push(target);
388
+ if (record$6(targets)) {
389
+ for (const target of Object.values(targets)) if (record$6(target)) dependencyOwners.push(target);
382
390
  }
383
391
  for (const owner of dependencyOwners) for (const key of [
384
392
  "dependencies",
@@ -386,12 +394,12 @@ const workspaceRoots = (files, ecosystem) => {
386
394
  "build-dependencies"
387
395
  ]) {
388
396
  const dependencies = owner[key];
389
- if (!record$5(dependencies)) continue;
397
+ if (!record$6(dependencies)) continue;
390
398
  for (const [name, dependency] of Object.entries(dependencies)) {
391
- if (!record$5(dependency)) continue;
399
+ if (!record$6(dependency)) continue;
392
400
  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;
401
+ const resolved = isInherited && record$6(inherited) ? inherited[name] : dependency;
402
+ if (!record$6(resolved) || typeof resolved["path"] !== "string") continue;
395
403
  if (node_path.default.posix.isAbsolute(resolved["path"]) || /\\|\$|^[A-Za-z]:/.test(resolved["path"])) continue;
396
404
  const target = node_path.default.posix.normalize(node_path.default.posix.join(isInherited ? root : directory, resolved["path"]));
397
405
  const relative = node_path.default.posix.relative(root, target);
@@ -419,7 +427,7 @@ const javascriptEcosystem = (directory, files) => {
419
427
  if (files[node_path.default.posix.join(root, "bun.lock")] !== void 0) return "bun";
420
428
  try {
421
429
  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";
430
+ if (record$6(manifest) && typeof manifest["packageManager"] === "string" && manifest["packageManager"].startsWith("bun@")) return "bun";
423
431
  } catch {}
424
432
  return "npm";
425
433
  };
@@ -431,7 +439,7 @@ const pythonEcosystem = (file, files) => {
431
439
  const pyproject = files[node_path.default.posix.join(root, "pyproject.toml")];
432
440
  if (pyproject !== void 0) try {
433
441
  const tool = (0, smol_toml.parse)(pyproject)["tool"];
434
- if (record$5(tool) && record$5(tool["uv"])) return "uv";
442
+ if (record$6(tool) && record$6(tool["uv"])) return "uv";
435
443
  } catch {}
436
444
  return "pip";
437
445
  };
@@ -502,7 +510,7 @@ const checkDependabot = ({ files, policy }) => {
502
510
  add("cannot resolve Dependabot YAML aliases");
503
511
  return diagnostics;
504
512
  }
505
- if (!record$5(parsed) || parsed["version"] !== 2 || !Array.isArray(parsed["updates"])) {
513
+ if (!record$6(parsed) || parsed["version"] !== 2 || !Array.isArray(parsed["updates"])) {
506
514
  add("Dependabot must declare version: 2 and an updates array");
507
515
  return diagnostics;
508
516
  }
@@ -515,7 +523,7 @@ const checkDependabot = ({ files, policy }) => {
515
523
  });
516
524
  if (legacyDocument.errors.length === 0) legacy = legacyDocument.toJS({ maxAliasCount: 100 });
517
525
  } catch {}
518
- const legacyUpdates = record$5(legacy) && Array.isArray(legacy["updates"]) ? legacy["updates"] : [];
526
+ const legacyUpdates = record$6(legacy) && Array.isArray(legacy["updates"]) ? legacy["updates"] : [];
519
527
  const rootClaims = /* @__PURE__ */ new Map();
520
528
  const directoryClaims = /* @__PURE__ */ new Map();
521
529
  for (const [index, update] of parsed["updates"].entries()) {
@@ -529,7 +537,7 @@ const checkDependabot = ({ files, policy }) => {
529
537
  return offset === void 0 ? 1 : lineCounter.linePos(offset).line;
530
538
  };
531
539
  const report = (key, message) => add(message, lineOf(key));
532
- if (!record$5(update) || typeof update["package-ecosystem"] !== "string") {
540
+ if (!record$6(update) || typeof update["package-ecosystem"] !== "string") {
533
541
  report("package-ecosystem", "each Dependabot update must declare a package ecosystem");
534
542
  continue;
535
543
  }
@@ -563,21 +571,22 @@ const checkDependabot = ({ files, policy }) => {
563
571
  covered.set(ecosystem, matches);
564
572
  const schedule = update["schedule"];
565
573
  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`);
574
+ const legacySchedule = record$6(legacyUpdate) ? legacyUpdate["schedule"] : void 0;
575
+ 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`);
576
+ 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
577
  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}`);
578
+ 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
579
  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]) => {
580
+ if (!record$6(groups) || Object.keys(groups).length !== Object.keys(policy.groups).length || Object.entries(policy.groups).some(([name, expected]) => {
573
581
  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);
582
+ return !record$6(group) || Object.keys(group).length !== 2 || !sameStrings(group["patterns"], expected.patterns) || !sameStrings(group["update-types"], expected.updateTypes);
575
583
  })) report("groups", `${ecosystem}: groups must match the shared policy`);
576
584
  const ignored = ignoredFor(ecosystem, policy);
577
585
  const ignore = update["ignore"];
578
586
  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}`);
587
+ 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`);
588
+ if (Array.isArray(ignore) && ignore.some((entry) => !versionOnlyIgnore(entry))) report("ignore", `${ecosystem}: each shared-pin ignore must contain only dependency-name and update-types for semver-major, semver-minor, and semver-patch version updates; security updates must remain enabled`);
589
+ for (const name of ignored) if (!Array.isArray(ignore) || !ignore.some((entry) => record$6(entry) && entry["dependency-name"] === name && versionOnlyIgnore(entry))) report("ignore", `${ecosystem}: ignore all version updates for shared pin ${name}`);
581
590
  }
582
591
  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
592
  return diagnostics;
@@ -589,7 +598,7 @@ const locatorValidators = {
589
598
  "bun-source": /^(?:jobs\.[\w-]+|runs)\.steps\.[\w-]+$/
590
599
  };
591
600
  const knownKind = (value) => typeof value === "string" && Object.hasOwn(locatorValidators, value);
592
- const record$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
601
+ const record$5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
593
602
  /** One reviewed declaration identifies one unresolved selector, never a rule. */
594
603
  const parseDynamicSelectors = (input) => {
595
604
  if (input === void 0) return [];
@@ -597,7 +606,7 @@ const parseDynamicSelectors = (input) => {
597
606
  const result = [];
598
607
  const keys = /* @__PURE__ */ new Set();
599
608
  for (const entry of input) {
600
- if (!record$4(entry) || Object.keys(entry).some((key) => ![
609
+ if (!record$5(entry) || Object.keys(entry).some((key) => ![
601
610
  "path",
602
611
  "kind",
603
612
  "reason",
@@ -633,7 +642,7 @@ const parseDynamicSelectors = (input) => {
633
642
  };
634
643
  //#endregion
635
644
  //#region src/toolchain-engine-floors.ts
636
- const record$3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
645
+ const record$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
637
646
  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
647
  const parseEngineFloors = (input) => {
639
648
  if (input === void 0) return [];
@@ -641,7 +650,7 @@ const parseEngineFloors = (input) => {
641
650
  const result = [];
642
651
  const jobs = /* @__PURE__ */ new Set();
643
652
  for (const entry of input) {
644
- if (!record$3(entry) || Object.keys(entry).some((key) => ![
653
+ if (!record$4(entry) || Object.keys(entry).some((key) => ![
645
654
  "package",
646
655
  "workflow",
647
656
  "job"
@@ -662,16 +671,16 @@ const resolveEngineFloor = (entry, files) => {
662
671
  const workflowText = files[entry.workflow];
663
672
  if (manifestText === void 0 || workflowText === void 0) throw new Error("engine floor package and workflow must be tracked");
664
673
  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");
674
+ 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
675
  const range = manifest["engines"]["node"];
667
676
  const minimum = (0, semver.validRange)(range) === null ? null : (0, semver.minVersion)(range);
668
677
  if (minimum === null || minimum.prerelease.length !== 0) throw new Error("engine floor engines.node must declare a stable minimum");
669
678
  const document = (0, yaml.parseDocument)(workflowText, { merge: true });
670
679
  if (document.errors.length !== 0) throw new Error("engine floor workflow must be valid YAML");
671
680
  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");
681
+ if (!record$4(workflow) || !record$4(workflow["jobs"])) throw new Error("engine floor job must exist in its tracked workflow");
673
682
  const job = workflow["jobs"][entry.job];
674
- if (!record$3(job)) throw new Error("engine floor job must exist in its tracked workflow");
683
+ if (!record$4(job)) throw new Error("engine floor job must exist in its tracked workflow");
675
684
  if (!Array.isArray(job["steps"]) || Object.hasOwn(job, "uses")) throw new Error("engine floor job must declare steps and cannot reuse a workflow");
676
685
  return {
677
686
  ...entry,
@@ -691,7 +700,7 @@ const packageRules = [
691
700
  "typescript-layout",
692
701
  "node-engine"
693
702
  ];
694
- const record$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
703
+ const record$3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
695
704
  const shellSubstitutions = (command) => {
696
705
  const frames = [{
697
706
  kind: "root",
@@ -752,43 +761,371 @@ const shellSubstitutions = (command) => {
752
761
  for (const frame of frames.slice(1)) bodies.push(command.slice(frame.start));
753
762
  return bodies;
754
763
  };
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);
764
+ const shellValueOptions = /* @__PURE__ */ new Set([
765
+ "--rcfile",
766
+ "--init-file",
767
+ "-o",
768
+ "+o",
769
+ "-O",
770
+ "+O"
771
+ ]);
772
+ const shellExecutable = /^(?:\/(?:[^/]+\/)*)?(?:bash|dash|ksh|zsh|sh)$/;
773
+ const shellTokens = (source) => {
774
+ const tokens = [];
775
+ for (let index = 0; index < source.length;) {
776
+ const character = source.at(index);
777
+ if (character === void 0) break;
778
+ if (character === "\n") {
779
+ tokens.push({
780
+ kind: "boundary",
781
+ value: character,
782
+ index: index++
783
+ });
784
+ continue;
785
+ }
786
+ if (/\s/.test(character)) {
787
+ index++;
788
+ continue;
789
+ }
790
+ if (character === "#") {
791
+ const end = source.indexOf("\n", index);
792
+ const stop = end < 0 ? source.length : end;
793
+ tokens.push({
794
+ kind: "comment",
795
+ value: source.slice(index, stop),
796
+ index
797
+ });
798
+ index = stop;
799
+ continue;
800
+ }
801
+ const redirect = /^(?:\d*)(?:&>>|&>|<<<|<<-|<<|>>|>\||>&|<&|<>|>|<)/.exec(source.slice(index));
802
+ if (redirect !== null) {
803
+ tokens.push({
804
+ kind: "redirect",
805
+ value: redirect[0],
806
+ index
807
+ });
808
+ index += redirect[0].length;
809
+ continue;
810
+ }
811
+ const boundary = /^(?:&&|\|\||[();&|])/.exec(source.slice(index));
812
+ if (boundary !== null) {
813
+ tokens.push({
814
+ kind: "boundary",
815
+ value: boundary[0],
816
+ index
817
+ });
818
+ index += boundary[0].length;
819
+ continue;
820
+ }
821
+ const start = index;
822
+ const frames = [{
823
+ kind: "word",
824
+ quote: void 0,
825
+ depth: 0
826
+ }];
827
+ while (index < source.length) {
828
+ const frame = frames.at(-1);
829
+ const current = source.at(index);
830
+ if (frame === void 0 || current === void 0) break;
831
+ if (current === "\\" && frame.quote !== "'") {
832
+ index += 2;
833
+ continue;
834
+ }
835
+ if (current === "`" && frame.kind === "backticks") {
836
+ frames.pop();
837
+ index++;
838
+ continue;
839
+ }
840
+ if (current === frame.quote) {
841
+ frame.quote = void 0;
842
+ index++;
843
+ continue;
844
+ }
845
+ if (frame.quote === "'") {
846
+ index++;
847
+ continue;
848
+ }
849
+ if (current === "$" && source.at(index + 1) === "(") {
850
+ frames.push({
851
+ kind: "parentheses",
852
+ quote: void 0,
853
+ depth: 1
854
+ });
855
+ index += 2;
856
+ continue;
857
+ }
858
+ if (current === "`") {
859
+ frames.push({
860
+ kind: "backticks",
861
+ quote: void 0,
862
+ depth: 0
863
+ });
864
+ index++;
865
+ continue;
866
+ }
867
+ if (frame.quote !== void 0) {
868
+ index++;
869
+ continue;
870
+ }
871
+ if (current === "'" || current === "\"") {
872
+ frame.quote = current;
873
+ index++;
874
+ continue;
875
+ }
876
+ if (frame.kind === "parentheses") {
877
+ if (current === "(") frame.depth++;
878
+ if (current === ")" && --frame.depth === 0) frames.pop();
879
+ index++;
880
+ continue;
881
+ }
882
+ if (frame.kind === "word" && /[\s<>;&|()]/.test(current)) break;
883
+ index++;
884
+ }
885
+ const value = source.slice(start, index);
886
+ tokens.push({
887
+ kind: "word",
888
+ value,
889
+ index: start
890
+ });
762
891
  }
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") {
892
+ let groupDepth = 0;
893
+ let redirectTarget = false;
894
+ const commandWords = [];
895
+ for (const token of tokens) {
896
+ if (token.kind === "comment") continue;
897
+ if (token.kind === "redirect") {
898
+ redirectTarget = true;
899
+ continue;
900
+ }
901
+ if (redirectTarget) {
902
+ redirectTarget = false;
903
+ if (token.kind === "word") continue;
904
+ }
905
+ if (token.kind === "boundary") {
906
+ commandWords.length = 0;
907
+ continue;
908
+ }
909
+ const functionBody = commandWords.length === 2 && commandWords.at(0) === "function";
910
+ const opensGroup = token.value === "{" && (commandWords.length === 0 || functionBody);
911
+ const closesGroup = token.value === "}" && groupDepth > 0 && commandWords.length === 0;
912
+ if (opensGroup || closesGroup) {
913
+ token.kind = "boundary";
914
+ groupDepth += opensGroup ? 1 : -1;
915
+ commandWords.length = 0;
916
+ continue;
917
+ }
918
+ if (commandWords.length === 0 && [
919
+ "if",
920
+ "then",
921
+ "elif",
922
+ "else",
923
+ "do",
924
+ "!",
925
+ "time"
926
+ ].includes(token.value)) continue;
927
+ commandWords.push(token.value);
928
+ }
929
+ return tokens;
930
+ };
931
+ const shellArgumentTokens = (tokens) => {
932
+ const arguments_ = [];
933
+ let redirectTarget = false;
934
+ for (const token of tokens) {
935
+ if (token.kind === "comment") continue;
936
+ if (token.kind === "redirect") {
937
+ redirectTarget = true;
938
+ continue;
939
+ }
940
+ if (redirectTarget) {
941
+ redirectTarget = false;
942
+ if (token.kind === "word") continue;
943
+ }
944
+ arguments_.push(token);
945
+ }
946
+ return arguments_;
947
+ };
948
+ const shellReadsProgramStdin = (tokens) => {
949
+ const words = executableShellWords(shellArgumentTokens(tokens).map(({ value }) => value)).map(decodeShellWord);
950
+ if (!shellExecutable.test(words.at(0) ?? "")) return false;
951
+ let stdin = false;
952
+ let script = false;
953
+ for (let index = 1; index < words.length; index++) {
954
+ const option = words.at(index) ?? "";
955
+ if (option === "--" || option === "-") {
956
+ script = index + 1 < words.length;
957
+ break;
958
+ }
959
+ if (!/^[+-]/.test(option)) {
960
+ script = true;
961
+ break;
962
+ }
963
+ if (shellValueOptions.has(option)) {
964
+ index++;
965
+ continue;
966
+ }
967
+ if (/^-[^-]*c/.test(option)) return false;
968
+ if (/^-[^-]*s/.test(option)) stdin = true;
969
+ }
970
+ return stdin || !script;
971
+ };
972
+ const shellStdinHeredoc = (tokens) => {
973
+ const descriptors = /* @__PURE__ */ new Map();
974
+ for (let index = 0; index < tokens.length; index++) {
975
+ const token = tokens.at(index);
976
+ if (token?.kind !== "redirect") continue;
977
+ const declaration = /^(\d*)(.+)$/.exec(token.value);
978
+ const operator = declaration?.[2];
979
+ if (operator === void 0) continue;
980
+ const descriptor = String(Number(declaration?.[1] || (operator.startsWith("<") ? "0" : "1")));
981
+ const target = tokens.at(++index);
982
+ if (operator === "<<" || operator === "<<-") {
983
+ descriptors.set(descriptor, token);
984
+ continue;
985
+ }
986
+ if (operator === "<&" || operator === ">&") {
987
+ const copy = /^(\d+)(-?)$/.exec(decodeShellWord(target?.value ?? ""));
988
+ const sourceDescriptor = copy?.[1] === void 0 ? void 0 : String(Number(copy[1]));
989
+ const source = sourceDescriptor === void 0 ? void 0 : descriptors.get(sourceDescriptor);
990
+ if (source === void 0) descriptors.delete(descriptor);
991
+ else descriptors.set(descriptor, source);
992
+ if (copy?.[2] === "-" && sourceDescriptor !== void 0) descriptors.delete(sourceDescriptor);
993
+ continue;
994
+ }
995
+ descriptors.delete(descriptor);
996
+ if (operator === "&>" || operator === "&>>") descriptors.delete("2");
997
+ }
998
+ return descriptors.get("0");
999
+ };
1000
+ const shellExecutableSource = (command) => {
1001
+ const pending = [];
1002
+ return command.split(/\r?\n/).map((line) => {
1003
+ const here = pending.at(0);
1004
+ if (here !== void 0) {
1005
+ if ((here.stripTabs ? line.replace(/^\t+/, "") : line) === here.delimiter) {
1006
+ pending.shift();
1007
+ return "";
1008
+ }
1009
+ if (here.execute) return line;
1010
+ if (here.literal) return "";
1011
+ return shellSubstitutions(line).join("\n");
1012
+ }
1013
+ const words = shellTokens(line);
1014
+ let commandStart = 0;
1015
+ for (let index = 0; index < words.length; index++) {
1016
+ const match = words.at(index);
1017
+ const word = match?.value;
1018
+ if (word === void 0) continue;
1019
+ if (match?.kind === "comment") return line.slice(0, match.index);
1020
+ if (match?.kind === "boundary") {
1021
+ commandStart = index + 1;
1022
+ continue;
1023
+ }
1024
+ const declaration = match?.kind === "redirect" ? /^(?:\d*)<<(-?)(.*)$/.exec(word) : null;
1025
+ let delimiter = declaration?.[2];
1026
+ if (delimiter === "") delimiter = words.at(++index)?.value;
1027
+ if (declaration !== null && delimiter !== void 0 && !delimiter.startsWith("<")) {
1028
+ const commandEnd = words.findIndex((entry, position) => position > index && (entry.kind === "boundary" || entry.kind === "comment"));
1029
+ const invocation = words.slice(commandStart, commandEnd < 0 ? void 0 : commandEnd);
1030
+ pending.push({
1031
+ delimiter: decodeShellWord(delimiter),
1032
+ stripTabs: declaration[1] === "-",
1033
+ literal: /['"\\]/.test(delimiter),
1034
+ execute: shellReadsProgramStdin(invocation) && shellStdinHeredoc(invocation) === match
1035
+ });
1036
+ }
1037
+ }
1038
+ return line;
1039
+ }).join("\n");
1040
+ };
1041
+ const executableShellWords = (words) => {
1042
+ let start = 0;
1043
+ const skipAssignments = () => {
1044
+ while (/^[A-Za-z_][A-Za-z\d_]*=/.test(decodeShellWord(words.at(start) ?? ""))) start += 1;
1045
+ };
1046
+ skipAssignments();
1047
+ while (/^(?:\/usr\/bin\/|\/bin\/)?env$/.test(decodeShellWord(words.at(start) ?? ""))) {
1048
+ start += 1;
1049
+ while (start < words.length) {
1050
+ const option = decodeShellWord(words.at(start) ?? "");
1051
+ if (option === "--") {
1052
+ start += 1;
1053
+ break;
1054
+ }
1055
+ if ([
1056
+ "-u",
1057
+ "--unset",
1058
+ "-C",
1059
+ "--chdir"
1060
+ ].includes(option)) {
1061
+ start += 2;
1062
+ continue;
1063
+ }
1064
+ if (option === "-S" || option === "--split-string" || option.startsWith("--split-string=") || option.startsWith("-S") && option.length > 2) {
1065
+ let source;
1066
+ if (option === "-S" || option === "--split-string") {
1067
+ source = decodeShellWord(words.at(start + 1) ?? "");
778
1068
  start += 2;
779
- continue;
1069
+ } else {
1070
+ source = option.startsWith("-S") ? option.slice(2) : option.slice(15);
1071
+ start++;
780
1072
  }
781
- if (option === "-" || option === "-i" || option === "--ignore-environment" || /^--unset=.+|^-u.+/.test(option ?? "")) {
782
- start += 1;
783
- continue;
784
- }
785
- break;
1073
+ return executableShellWords([
1074
+ "env",
1075
+ ...shellTokens(source).map(({ value }) => value),
1076
+ ...words.slice(start)
1077
+ ]);
786
1078
  }
787
- skipAssignments();
1079
+ if (option === "-" || option === "-i" || option === "--ignore-environment" || /^--(?:unset|chdir)=.+|^-[uC].+/.test(option)) {
1080
+ start += 1;
1081
+ continue;
1082
+ }
1083
+ break;
788
1084
  }
789
- return words.slice(start);
790
- });
1085
+ skipAssignments();
1086
+ }
1087
+ return words.slice(start);
791
1088
  };
1089
+ const shellCommandBody = (rawWords) => {
1090
+ const words = executableShellWords(rawWords.filter((word) => word !== "(" && word !== ")")).map(decodeShellWord);
1091
+ if (!shellExecutable.test(words.at(0) ?? "")) return void 0;
1092
+ for (let index = 1; index < words.length; index++) {
1093
+ const option = words.at(index);
1094
+ if (option === void 0 || option === "--" || !/^[+-]/.test(option)) return void 0;
1095
+ if (shellValueOptions.has(option)) {
1096
+ index++;
1097
+ continue;
1098
+ }
1099
+ if (/^-[^-]*c/.test(option)) return words.at(index + 1);
1100
+ }
1101
+ };
1102
+ /** Shared executable shell words preserve assignments for runtime selectors. */
1103
+ const shellCommandSegments = (command, grouping = "boundaries") => {
1104
+ const segments = [[]];
1105
+ const executableSource = shellExecutableSource(command);
1106
+ const sources = /* @__PURE__ */ new Set([executableSource, ...shellSubstitutions(executableSource)]);
1107
+ for (const source of sources) {
1108
+ const firstSegment = segments.length;
1109
+ segments.push([]);
1110
+ for (const { kind, value: word } of shellArgumentTokens(shellTokens(source))) if (kind === "boundary") if ([
1111
+ "(",
1112
+ ")",
1113
+ "{",
1114
+ "}"
1115
+ ].includes(word) && grouping === "tokens") segments.at(-1)?.push(word);
1116
+ else segments.push([]);
1117
+ else segments.at(-1)?.push(word);
1118
+ for (const words of segments.slice(firstSegment)) {
1119
+ const body = shellCommandBody(words);
1120
+ if (body === void 0) continue;
1121
+ const executableBody = shellExecutableSource(body);
1122
+ sources.add(executableBody);
1123
+ for (const substitution of shellSubstitutions(executableBody)) sources.add(substitution);
1124
+ }
1125
+ }
1126
+ return segments;
1127
+ };
1128
+ const compilerCommandSegments = (command) => shellCommandSegments(command, "tokens").map(executableShellWords);
792
1129
  const launcherValueOptions = new Map(Object.entries({
793
1130
  npx: [
794
1131
  "-p",
@@ -922,10 +1259,10 @@ const checkPackageFiles = ({ files, policy }) => {
922
1259
  const document = pnpm ? (0, yaml.parseDocument)(text, { lineCounter }) : void 0;
923
1260
  if (document !== void 0 && document.errors.length > 0) throw new Error("invalid YAML");
924
1261
  const json = document === void 0 ? JSON.parse(text) : document.toJS();
925
- if (!record$2(json)) throw new Error("manifest must be an object");
1262
+ if (!record$3(json)) throw new Error("manifest must be an object");
926
1263
  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");
1264
+ if (json["catalog"] !== void 0 && !record$3(json["catalog"])) throw new Error("catalog must be a mapping");
1265
+ 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
1266
  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
1267
  }
931
1268
  manifests.set(file, json);
@@ -939,13 +1276,13 @@ const checkPackageFiles = ({ files, policy }) => {
939
1276
  catalog: json["overrides"],
940
1277
  prefix: ["overrides"]
941
1278
  },
942
- ...record$2(json["catalogs"]) ? Object.entries(json["catalogs"]).map(([name, catalog]) => ({
1279
+ ...record$3(json["catalogs"]) ? Object.entries(json["catalogs"]).map(([name, catalog]) => ({
943
1280
  catalog,
944
1281
  prefix: ["catalogs", name]
945
1282
  })) : []
946
1283
  ];
947
1284
  for (const { catalog, prefix } of catalogEntries) {
948
- if (!record$2(catalog)) continue;
1285
+ if (!record$3(catalog)) continue;
949
1286
  for (const [name, value] of Object.entries(catalog)) {
950
1287
  const node = document.getIn([...prefix, name], true);
951
1288
  if (!(0, yaml.isNode)(node) || node.range === void 0 || node.range === null) continue;
@@ -964,7 +1301,7 @@ const checkPackageFiles = ({ files, policy }) => {
964
1301
  });
965
1302
  }
966
1303
  }
967
- const catalogSources = (json) => [json, ...record$2(json["workspaces"]) ? [json["workspaces"]] : []];
1304
+ const catalogSources = (json) => [json, ...record$3(json["workspaces"]) ? [json["workspaces"]] : []];
968
1305
  const pnpmWorkspaceFor = (file) => {
969
1306
  let directory = node_path.default.posix.dirname(file);
970
1307
  while (true) {
@@ -987,8 +1324,8 @@ const checkPackageFiles = ({ files, policy }) => {
987
1324
  patterns: workspace["packages"],
988
1325
  rootMembership: "implicit"
989
1326
  })) 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;
1327
+ if (name === "" || name === "default") return workspace["catalog"] ?? (record$3(workspace["catalogs"]) ? workspace["catalogs"]["default"] : void 0);
1328
+ return record$3(workspace["catalogs"]) ? workspace["catalogs"][name] : void 0;
992
1329
  }
993
1330
  let directory = node_path.default.posix.dirname(file);
994
1331
  while (true) {
@@ -996,7 +1333,7 @@ const checkPackageFiles = ({ files, policy }) => {
996
1333
  if (manifest !== void 0) {
997
1334
  for (const source of catalogSources(manifest)) {
998
1335
  if (name === "" && source["catalog"] !== void 0) return source["catalog"];
999
- if (record$2(source["catalogs"]) && source["catalogs"][name] !== void 0) return source["catalogs"][name];
1336
+ if (record$3(source["catalogs"]) && source["catalogs"][name] !== void 0) return source["catalogs"][name];
1000
1337
  }
1001
1338
  if (manifest["workspaces"] !== void 0) return void 0;
1002
1339
  }
@@ -1010,7 +1347,7 @@ const checkPackageFiles = ({ files, policy }) => {
1010
1347
  if (visited.has(value)) return void 0;
1011
1348
  visited.add(value);
1012
1349
  const catalog = catalogFor(file, value.slice(8));
1013
- value = record$2(catalog) ? catalog[name] : void 0;
1350
+ value = record$3(catalog) ? catalog[name] : void 0;
1014
1351
  }
1015
1352
  return value;
1016
1353
  };
@@ -1026,7 +1363,7 @@ const checkPackageFiles = ({ files, policy }) => {
1026
1363
  const workspaces = manifests.get(node_path.default.posix.join(directory, "package.json"))?.["workspaces"];
1027
1364
  if (workspaces !== void 0) return {
1028
1365
  directory,
1029
- patterns: record$2(workspaces) ? workspaces["packages"] : workspaces,
1366
+ patterns: record$3(workspaces) ? workspaces["packages"] : workspaces,
1030
1367
  source: "json"
1031
1368
  };
1032
1369
  if (directory === ".") return void 0;
@@ -1082,7 +1419,7 @@ const checkPackageFiles = ({ files, policy }) => {
1082
1419
  "peerDependencies"
1083
1420
  ]) {
1084
1421
  const dependencies = json[section];
1085
- if (!record$2(dependencies)) continue;
1422
+ if (!record$3(dependencies)) continue;
1086
1423
  for (const [name, value] of Object.entries(dependencies)) {
1087
1424
  if (!tsSpecifiers.has(name)) continue;
1088
1425
  const resolved = resolve(file, name, value);
@@ -1144,7 +1481,7 @@ const checkPackageFiles = ({ files, policy }) => {
1144
1481
  message: `packageManager Bun version must be ${policy.bun}, found ${JSON.stringify(manager)}`
1145
1482
  });
1146
1483
  const checkEntries = (entries, location) => {
1147
- if (!record$2(entries)) return;
1484
+ if (!record$3(entries)) return;
1148
1485
  for (const [name, raw] of Object.entries(entries)) {
1149
1486
  const value = resolve(file, name, raw);
1150
1487
  const tsAllowed = tsSpecifiers.get(name);
@@ -1181,12 +1518,12 @@ const checkPackageFiles = ({ files, policy }) => {
1181
1518
  "devDependencies",
1182
1519
  "optionalDependencies"
1183
1520
  ]) checkEntries(json[key]);
1184
- if (record$2(json["peerDependencies"])) {
1521
+ if (record$3(json["peerDependencies"])) {
1185
1522
  for (const [name, value] of Object.entries(json["peerDependencies"])) if (tsSpecifiers.has(name) && !peerRange(resolve(file, name, value))) checkEntries({ [name]: value });
1186
1523
  }
1187
1524
  for (const source of catalogSources(json)) {
1188
1525
  checkEntries(source["catalog"]);
1189
- if (!record$2(source["catalogs"])) continue;
1526
+ if (!record$3(source["catalogs"])) continue;
1190
1527
  for (const catalog of Object.values(source["catalogs"])) checkEntries(catalog);
1191
1528
  }
1192
1529
  const ownedNames = /* @__PURE__ */ new Set([...pins.keys(), ...tsSpecifiers.keys()]);
@@ -1198,7 +1535,7 @@ const checkPackageFiles = ({ files, policy }) => {
1198
1535
  "optionalDependencies"
1199
1536
  ]) {
1200
1537
  const entries = resolutionRoot[section];
1201
- if (!record$2(entries)) continue;
1538
+ if (!record$3(entries)) continue;
1202
1539
  for (const [name, value] of Object.entries(entries)) resolutionDependencies[name] = resolve(file, name, value);
1203
1540
  }
1204
1541
  const affectedSpecifiers = /* @__PURE__ */ new Map();
@@ -1218,7 +1555,7 @@ const checkPackageFiles = ({ files, policy }) => {
1218
1555
  "optionalDependencies"
1219
1556
  ]) {
1220
1557
  const entries = member[section];
1221
- if (!record$2(entries)) continue;
1558
+ if (!record$3(entries)) continue;
1222
1559
  for (const [name, value] of Object.entries(entries)) {
1223
1560
  if (!tsSpecifiers.has(name)) continue;
1224
1561
  const values = affectedSpecifiers.get(name) ?? /* @__PURE__ */ new Set();
@@ -1228,7 +1565,7 @@ const checkPackageFiles = ({ files, policy }) => {
1228
1565
  }
1229
1566
  }
1230
1567
  const checkResolutionMap = (entries, parent) => {
1231
- if (!record$2(entries)) return;
1568
+ if (!record$3(entries)) return;
1232
1569
  for (const [selector, replacement] of Object.entries(entries)) {
1233
1570
  const target = selector === "." ? parent : selector;
1234
1571
  if (target === void 0) continue;
@@ -1251,7 +1588,7 @@ const checkPackageFiles = ({ files, policy }) => {
1251
1588
  });
1252
1589
  continue;
1253
1590
  }
1254
- if (record$2(replacement)) {
1591
+ if (record$3(replacement)) {
1255
1592
  checkResolutionMap(replacement, target);
1256
1593
  continue;
1257
1594
  }
@@ -1285,7 +1622,7 @@ const checkPackageFiles = ({ files, policy }) => {
1285
1622
  };
1286
1623
  checkResolutionMap(json["overrides"]);
1287
1624
  checkResolutionMap(json["resolutions"]);
1288
- if (record$2(json["pnpm"])) checkResolutionMap(json["pnpm"]["overrides"]);
1625
+ if (record$3(json["pnpm"])) checkResolutionMap(json["pnpm"]["overrides"]);
1289
1626
  const localEntries = toolchainEntries(file, json);
1290
1627
  const usesCompiler = policy.typescriptInstallLayouts.some((layout) => localEntries.some(({ name }) => name === layout.compilerPackage));
1291
1628
  let selectedLayout = layoutFor(specifierMap(localEntries));
@@ -1309,7 +1646,7 @@ const checkPackageFiles = ({ files, policy }) => {
1309
1646
  key: "dependencies",
1310
1647
  message: "TypeScript compiler and compatibility packages must match one complete declared install layout"
1311
1648
  });
1312
- const command = record$2(json["scripts"]) ? json["scripts"]["typecheck"] : void 0;
1649
+ const command = record$3(json["scripts"]) ? json["scripts"]["typecheck"] : void 0;
1313
1650
  if (selectedLayout !== void 0 && typeof command === "string") {
1314
1651
  const normalizedExpected = selectedLayout.typecheckCommand.replace(/\s+/g, " ").trim();
1315
1652
  for (const words of compilerCommandSegments(command)) {
@@ -1327,7 +1664,7 @@ const checkPackageFiles = ({ files, policy }) => {
1327
1664
  }
1328
1665
  }
1329
1666
  }
1330
- if (record$2(json["peerDependencies"])) {
1667
+ if (record$3(json["peerDependencies"])) {
1331
1668
  const peerLayout = selectedLayout ?? workspaceLayoutFor(file)?.layout;
1332
1669
  for (const [name, raw] of Object.entries(json["peerDependencies"])) {
1333
1670
  if (!tsSpecifiers.has(name)) continue;
@@ -1351,7 +1688,7 @@ const checkPackageFiles = ({ files, policy }) => {
1351
1688
  });
1352
1689
  }
1353
1690
  }
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({
1691
+ 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
1692
  file,
1356
1693
  rule: "node-engine",
1357
1694
  key: "node",
@@ -1362,6 +1699,232 @@ const checkPackageFiles = ({ files, policy }) => {
1362
1699
  return diagnostics;
1363
1700
  };
1364
1701
  //#endregion
1702
+ //#region src/toolchain-rust.ts
1703
+ const record$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1704
+ /** Exceptions authorize one immutable release, never a channel or an entire rule. */
1705
+ const parseRustExceptions = (input, today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)) => {
1706
+ if (input === void 0) return [];
1707
+ if (!Array.isArray(input)) throw new Error("rustExceptions must be an array");
1708
+ const pins = /* @__PURE__ */ new Set();
1709
+ return input.map((entry) => {
1710
+ if (!record$2(entry) || Object.keys(entry).length !== 4 || Object.keys(entry).some((key) => ![
1711
+ "pin",
1712
+ "reason",
1713
+ "tracking",
1714
+ "expires"
1715
+ ].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");
1716
+ pins.add(entry["pin"]);
1717
+ return {
1718
+ pin: entry["pin"],
1719
+ reason: entry["reason"],
1720
+ tracking: entry["tracking"],
1721
+ expires: entry["expires"]
1722
+ };
1723
+ });
1724
+ };
1725
+ /** Only executable fields are inspected: unrelated action inputs are not Rust selectors. */
1726
+ const checkRustReferences = (options) => {
1727
+ const { file, rust, exceptions } = options;
1728
+ const text = options.type === "file" ? options.text : "";
1729
+ const diagnostics = [];
1730
+ const select = (value, label, line = 1) => {
1731
+ if (value === rust || exceptions.some(({ pin }) => pin === value)) return;
1732
+ diagnostics.push({
1733
+ rule: "rust-version",
1734
+ path: file,
1735
+ line,
1736
+ message: `${label} must be Rust ${rust}, found ${String(value)}`
1737
+ });
1738
+ };
1739
+ const shell = (source, firstLine = 1) => {
1740
+ const commands = shellCommandSegments(source).map((words) => words.map((word) => ({
1741
+ value: decodeShellWord(word),
1742
+ line: firstLine
1743
+ })));
1744
+ for (const command of commands) {
1745
+ let position = 0;
1746
+ const prefixes = /* @__PURE__ */ new Set([
1747
+ "if",
1748
+ "then",
1749
+ "do",
1750
+ "elif",
1751
+ "!",
1752
+ "env",
1753
+ "sudo",
1754
+ "command",
1755
+ "exec",
1756
+ "time",
1757
+ "export",
1758
+ "readonly",
1759
+ "/usr/bin/env",
1760
+ "/bin/env"
1761
+ ]);
1762
+ while (position < command.length) {
1763
+ const value = command.at(position)?.value;
1764
+ if (value?.startsWith("RUSTUP_TOOLCHAIN=")) select(value.slice(17), "RUSTUP_TOOLCHAIN", firstLine);
1765
+ if (value === "-u" || value === "--unset") {
1766
+ position += 2;
1767
+ continue;
1768
+ }
1769
+ if (value?.startsWith("--unset=") || value === "-i" || value === "--ignore-environment") {
1770
+ position++;
1771
+ continue;
1772
+ }
1773
+ if (value !== void 0 && (prefixes.has(value) || /^[A-Za-z_][A-Za-z_\d]*=/.test(value))) position++;
1774
+ else break;
1775
+ }
1776
+ const executable = command.at(position)?.value;
1777
+ const arguments_ = command.slice(position + 1);
1778
+ if (executable === "cargo" && arguments_.at(0)?.value.startsWith("+")) {
1779
+ const selector = arguments_.at(0);
1780
+ select(selector?.value.slice(1), "cargo toolchain", selector?.line);
1781
+ }
1782
+ if (executable !== "rustup") continue;
1783
+ let subcommandPosition = 0;
1784
+ while (subcommandPosition < arguments_.length) {
1785
+ const token = arguments_.at(subcommandPosition);
1786
+ if (token === void 0) break;
1787
+ if (token.value === "--") {
1788
+ subcommandPosition++;
1789
+ break;
1790
+ }
1791
+ if (token.value.startsWith("+")) select(token.value.slice(1), "rustup toolchain", token.line);
1792
+ else if (!token.value.startsWith("-")) break;
1793
+ subcommandPosition++;
1794
+ }
1795
+ const invocation = arguments_.slice(subcommandPosition);
1796
+ const subcommand = invocation.at(0)?.value;
1797
+ if (subcommand === "target" || subcommand === "component") {
1798
+ for (let index = 1; index < invocation.length; index++) {
1799
+ const token = invocation.at(index);
1800
+ if (token?.value === "--toolchain") select(invocation.at(++index)?.value, "rustup --toolchain", token.line);
1801
+ else if (token?.value.startsWith("--toolchain=")) select(token.value.slice(12), "rustup --toolchain", token.line);
1802
+ }
1803
+ continue;
1804
+ }
1805
+ let selectors;
1806
+ let requiresSelector = true;
1807
+ let firstSelectorOnly = false;
1808
+ if (subcommand === "default" || subcommand === "install" || subcommand === "add" || subcommand === "update") {
1809
+ selectors = invocation.slice(1);
1810
+ requiresSelector = subcommand !== "update";
1811
+ } else if (subcommand === "toolchain" && [
1812
+ "install",
1813
+ "add",
1814
+ "update"
1815
+ ].includes(invocation.at(1)?.value ?? "")) {
1816
+ selectors = invocation.slice(2);
1817
+ requiresSelector = invocation.at(1)?.value !== "update";
1818
+ } else if (subcommand === "run") {
1819
+ selectors = invocation.slice(1);
1820
+ firstSelectorOnly = true;
1821
+ } else if (subcommand === "override" && invocation.at(1)?.value === "set") {
1822
+ selectors = invocation.slice(2);
1823
+ firstSelectorOnly = true;
1824
+ } else continue;
1825
+ let selected = 0;
1826
+ const optionArguments = /* @__PURE__ */ new Set([
1827
+ "--path",
1828
+ "--profile",
1829
+ "--component",
1830
+ "-c",
1831
+ "--target",
1832
+ "-t"
1833
+ ]);
1834
+ for (let index = 0; index < selectors.length; index++) {
1835
+ const token = selectors.at(index);
1836
+ if (token === void 0) continue;
1837
+ if (optionArguments.has(token.value)) {
1838
+ index++;
1839
+ continue;
1840
+ }
1841
+ if (token.value.startsWith("-")) continue;
1842
+ selected++;
1843
+ select(token.value, "rustup toolchain", token.line);
1844
+ if (firstSelectorOnly) break;
1845
+ }
1846
+ if (selected === 0 && requiresSelector) select(void 0, "rustup toolchain", firstLine);
1847
+ }
1848
+ };
1849
+ if (options.type === "environment") {
1850
+ for (const { key, value } of options.assignments) if (key === "RUSTUP_TOOLCHAIN") select(value, key, options.line);
1851
+ return diagnostics;
1852
+ }
1853
+ if (options.type === "command") {
1854
+ shell(options.source, options.line);
1855
+ return diagnostics;
1856
+ }
1857
+ const lineOf = (token) => {
1858
+ const index = text.split(/\r?\n/).findIndex((line) => line.includes(token));
1859
+ return index < 0 ? 1 : index + 1;
1860
+ };
1861
+ if (require_toolchain_inputs.isShellScript(file, text)) shell(text);
1862
+ if (/(?:^|\/)package\.json$/.test(file)) try {
1863
+ const object = JSON.parse(text);
1864
+ if (record$2(object) && record$2(object["scripts"])) {
1865
+ for (const [name, script] of Object.entries(object["scripts"])) if (typeof script === "string") shell(script, lineOf(`"${name}"`));
1866
+ }
1867
+ } catch {}
1868
+ if (/(?:^|\/)ci-service-images\.json$/.test(file)) {
1869
+ const images = (value) => {
1870
+ if (typeof value === "string" && /^rust(?=:|@|$)/.test(canonicalDockerRuntime(value))) {
1871
+ const tag = canonicalDockerRuntime(value).replace(/^rust:?/, "").split("@").at(0);
1872
+ const version = tag === void 0 ? void 0 : rustImageVersion(tag);
1873
+ select(version, "Rust service image", lineOf(value));
1874
+ } else if (Array.isArray(value)) value.forEach(images);
1875
+ else if (record$2(value)) Object.values(value).forEach(images);
1876
+ };
1877
+ try {
1878
+ images(JSON.parse(text));
1879
+ } catch {
1880
+ diagnostics.push({
1881
+ rule: "rust-version",
1882
+ path: file,
1883
+ line: 1,
1884
+ message: "invalid service image JSON"
1885
+ });
1886
+ }
1887
+ }
1888
+ if (require_toolchain_inputs.githubAutomationFileKind(file) === void 0) return diagnostics;
1889
+ const document = (0, yaml.parseDocument)(text);
1890
+ if (document.errors.length > 0) return diagnostics;
1891
+ try {
1892
+ const visit = (value) => {
1893
+ if (Array.isArray(value)) {
1894
+ value.forEach(visit);
1895
+ return;
1896
+ }
1897
+ if (!record$2(value)) return;
1898
+ if (record$2(value["env"]) && value["env"]["RUSTUP_TOOLCHAIN"] !== void 0) select(value["env"]["RUSTUP_TOOLCHAIN"], "RUSTUP_TOOLCHAIN", lineOf("RUSTUP_TOOLCHAIN"));
1899
+ if (typeof value["run"] === "string") shell(value["run"], lineOf("run:"));
1900
+ const uses = value["uses"];
1901
+ if (typeof uses === "string") {
1902
+ const action = uses.split("@").at(0)?.toLowerCase();
1903
+ const inputs = record$2(value["with"]) ? value["with"] : {};
1904
+ if (action === "dtolnay/rust-toolchain" || action === "actions-rust-lang/setup-rust-toolchain") {
1905
+ const entries = Object.entries(inputs).filter(([key]) => key.toLowerCase() === "toolchain");
1906
+ if (entries.length !== 1) select(void 0, `${action} requires an explicit exact toolchain input`, lineOf(uses));
1907
+ else {
1908
+ const selected = entries.at(0)?.at(1);
1909
+ const selectors = action === "actions-rust-lang/setup-rust-toolchain" && typeof selected === "string" ? selected.split(",").map((pin) => pin.trim()) : [selected];
1910
+ for (const selector of selectors) select(selector, `${action} toolchain`, lineOf("toolchain:"));
1911
+ }
1912
+ } else if (/(?:^|\/)\.github\/workflows\/[^@]+\.ya?ml(?:@|$)/.test(uses)) {
1913
+ for (const [key, selected] of Object.entries(inputs)) if (["toolchain", "rust-toolchain"].includes(key.toLowerCase())) select(selected, `reusable workflow ${key}`, lineOf(`${key}:`));
1914
+ }
1915
+ }
1916
+ for (const [key, child] of Object.entries(value)) {
1917
+ if (key === "workflow_call" && record$2(child) && record$2(child["inputs"])) {
1918
+ 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:"));
1919
+ }
1920
+ visit(child);
1921
+ }
1922
+ };
1923
+ visit(document.toJS({ maxAliasCount: 100 }));
1924
+ } catch {}
1925
+ return diagnostics;
1926
+ };
1927
+ //#endregion
1365
1928
  //#region src/toolchain-runtime.ts
1366
1929
  const runtimeRules = [
1367
1930
  "bun-pins",
@@ -1374,6 +1937,69 @@ const runtimeRules = [
1374
1937
  "action-pins"
1375
1938
  ];
1376
1939
  const record$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1940
+ const dockerEnvironmentAssignments = ({ source, escape }) => {
1941
+ const words = [];
1942
+ let word = "";
1943
+ let quote;
1944
+ for (let index = 0; index < source.length; index++) {
1945
+ const character = source.at(index) ?? "";
1946
+ if (character === escape && quote !== "'") {
1947
+ word += character + (source.at(++index) ?? "");
1948
+ continue;
1949
+ }
1950
+ if (character === quote) quote = void 0;
1951
+ else if (quote === void 0 && (character === "'" || character === "\"")) quote = character;
1952
+ else if (quote === void 0 && /\s/.test(character)) {
1953
+ if (word !== "") words.push(word);
1954
+ word = "";
1955
+ continue;
1956
+ }
1957
+ word += character;
1958
+ }
1959
+ if (quote !== void 0) return void 0;
1960
+ if (word !== "") words.push(word);
1961
+ const decode = (raw) => {
1962
+ let value = "";
1963
+ let quoted;
1964
+ for (let index = 0; index < raw.length; index++) {
1965
+ const character = raw.at(index) ?? "";
1966
+ if (character === quoted) {
1967
+ quoted = void 0;
1968
+ continue;
1969
+ }
1970
+ if (quoted === void 0 && (character === "'" || character === "\"")) {
1971
+ quoted = character;
1972
+ continue;
1973
+ }
1974
+ const next = raw.at(index + 1);
1975
+ if (character === escape && quoted !== "'" && next !== void 0 && (quoted === void 0 || next === "\"" || next === "$" || next === escape)) {
1976
+ value += next;
1977
+ index++;
1978
+ } else value += character;
1979
+ }
1980
+ return value;
1981
+ };
1982
+ const first = words.at(0);
1983
+ if (first === void 0) return void 0;
1984
+ if (!first.includes("=")) {
1985
+ const legacy = /^(\S+)\s+([\s\S]*)$/.exec(source);
1986
+ if (legacy === null) return void 0;
1987
+ return [{
1988
+ key: decode(legacy[1] ?? ""),
1989
+ value: decode(legacy[2] ?? "")
1990
+ }];
1991
+ }
1992
+ const assignments = [];
1993
+ for (const raw of words) {
1994
+ const separator = raw.indexOf("=");
1995
+ if (separator < 1) return void 0;
1996
+ assignments.push({
1997
+ key: decode(raw.slice(0, separator)),
1998
+ value: decode(raw.slice(separator + 1))
1999
+ });
2000
+ }
2001
+ return assignments;
2002
+ };
1377
2003
  const pythonSelectorMatches = (value, expected) => value === expected || typeof value === "string" && /^\d+\.\d+$/.test(expected) && value.startsWith(`${expected}.`) && /^\d+\.\d+\.\d+$/.test(value);
1378
2004
  const compareRelease = (left, right) => {
1379
2005
  for (let index = 0; index < Math.max(left.length, right.length); index++) {
@@ -1443,8 +2069,14 @@ const pythonRequirementMatches = (requirement, version) => {
1443
2069
  });
1444
2070
  };
1445
2071
  /** 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 = [];
2072
+ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, repository, onDelegated, dynamicSelectors = [], onDynamicSelector, rustExceptions = [], engineFloors = [], onEngineFloor }) => {
2073
+ const diagnostics = checkRustReferences({
2074
+ type: "file",
2075
+ file,
2076
+ text,
2077
+ rust: policy.rust,
2078
+ exceptions: rustExceptions
2079
+ });
1448
2080
  const name = node_path.default.posix.basename(file);
1449
2081
  const lines = text.split(/\r?\n/);
1450
2082
  const add = ({ rule, line, message }) => {
@@ -1467,7 +2099,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
1467
2099
  };
1468
2100
  const lineOf = (key) => Math.max(1, lines.findIndex((line) => line.includes(key)) + 1);
1469
2101
  const pin = ({ rule, value, expected, label, line = 1, selector }) => {
1470
- let matches = value === expected;
2102
+ let matches = value === expected || rule === "rust-version" && rustExceptions.some((entry) => entry.pin === value);
1471
2103
  if (selector === "node") matches = require_toolchain_inputs.nodeSelectorMatches(value, expected);
1472
2104
  else if (label.includes("python")) matches = pythonSelectorMatches(value, expected);
1473
2105
  if (!matches) add({
@@ -1537,12 +2169,10 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
1537
2169
  const parsed = toml("rust-version");
1538
2170
  if (parsed !== void 0) {
1539
2171
  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
2172
  pin({
1543
2173
  rule: "rust-version",
1544
2174
  value: record$1(toolchain) ? toolchain["channel"] : void 0,
1545
- expected,
2175
+ expected: policy.rust,
1546
2176
  label: "Rust channel",
1547
2177
  line: lineOf("channel")
1548
2178
  });
@@ -1643,7 +2273,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
1643
2273
  if (record$1(tools)) for (const [tool, expected] of Object.entries(versions)) {
1644
2274
  const configured = tools[tool];
1645
2275
  if (configured !== void 0) pin({
1646
- rule: "runtime-manager",
2276
+ rule: tool === "rust" || tool === "core:rust" ? "rust-version" : "runtime-manager",
1647
2277
  value: record$1(configured) ? configured["version"] : configured,
1648
2278
  expected,
1649
2279
  selector: nodeTools.has(tool) ? "node" : void 0,
@@ -1658,7 +2288,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
1658
2288
  if (tool === void 0 || !(tool in versions)) return;
1659
2289
  const expected = Object.entries(versions).find(([key]) => key === tool)?.[1];
1660
2290
  if (expected !== void 0) pin({
1661
- rule: "runtime-manager",
2291
+ rule: tool === "rust" || tool === "core:rust" ? "rust-version" : "runtime-manager",
1662
2292
  value: fields.slice(1).join(" "),
1663
2293
  expected,
1664
2294
  selector: nodeTools.has(tool) ? "node" : void 0,
@@ -1668,11 +2298,12 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
1668
2298
  });
1669
2299
  const checkRuntimeImage = ({ image, line, label, at }) => {
1670
2300
  const unknownRuntime = () => {
1671
- if (dynamic(image) && authorizeDynamic("image", line, at)) return;
2301
+ const rustImage = typeof image === "string" && /^rust(?=:|@|$)/i.test(canonicalDockerRuntime(image));
2302
+ if (!rustImage && dynamic(image) && authorizeDynamic("image", line, at)) return;
1672
2303
  add({
1673
- rule: "runtime-docker",
2304
+ rule: rustImage ? "rust-version" : "runtime-docker",
1674
2305
  line,
1675
- message: `cannot determine ${label} runtime; use a static image or a scoped dynamicSelectors declaration`
2306
+ 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
2307
  });
1677
2308
  };
1678
2309
  if (typeof image !== "string") {
@@ -1684,7 +2315,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
1684
2315
  const digestSeparator = structuralImage.indexOf("@");
1685
2316
  const reference = digestSeparator < 0 ? canonical : canonical.slice(0, digestSeparator);
1686
2317
  const structuralReference = digestSeparator < 0 ? structuralImage : structuralImage.slice(0, digestSeparator);
1687
- const managedFamily = /^(node|python|oven\/bun)(?=:|$)/i.exec(structuralReference)?.[1];
2318
+ const managedFamily = /^(node|python|rust|oven\/bun)(?=:|$)/i.exec(structuralReference)?.[1];
1688
2319
  const tagSeparator = managedFamily === void 0 ? structuralReference.lastIndexOf(":") : structuralReference.indexOf(":", managedFamily.length);
1689
2320
  const hasTag = managedFamily === void 0 ? tagSeparator > structuralReference.lastIndexOf("/") : tagSeparator !== -1;
1690
2321
  const imageRepository = hasTag ? reference.slice(0, tagSeparator) : reference;
@@ -1692,8 +2323,11 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
1692
2323
  unknownRuntime();
1693
2324
  return;
1694
2325
  }
1695
- if (!/^(node|python|oven\/bun)$/i.test(imageRepository)) return;
2326
+ if (!/^(node|python|rust|oven\/bun)$/i.test(imageRepository)) return;
1696
2327
  const tool = imageRepository.toLowerCase();
2328
+ let imageRule = "runtime-docker";
2329
+ if (tool === "rust") imageRule = "rust-version";
2330
+ else if (tool === "oven/bun") imageRule = "bun-pins";
1697
2331
  if (image.includes("$")) {
1698
2332
  unknownRuntime();
1699
2333
  return;
@@ -1701,7 +2335,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
1701
2335
  const digest = digestSeparator < 0 ? void 0 : canonical.slice(digestSeparator + 1);
1702
2336
  if (digest !== void 0 && !/^sha256:[a-fA-F\d]{64}$/.test(digest)) {
1703
2337
  add({
1704
- rule: tool === "oven/bun" ? "bun-pins" : "runtime-docker",
2338
+ rule: imageRule,
1705
2339
  line,
1706
2340
  message: `${label} ${tool} digest must be sha256 followed by 64 hexadecimal characters`
1707
2341
  });
@@ -1709,14 +2343,15 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
1709
2343
  }
1710
2344
  const tag = hasTag ? reference.slice(tagSeparator + 1) : void 0;
1711
2345
  const variantSuffix = tool === "oven/bun" ? /-(?:alpine|slim|debian)$/ : /-(?:alpine(?:\d+(?:\.\d+)*)?|(?:bookworm|bullseye|trixie|buster)(?:-slim)?|slim)$/;
1712
- const version = tag?.replace(variantSuffix, "");
2346
+ const version = tool === "rust" && tag !== void 0 ? rustImageVersion(tag) : tag?.replace(variantSuffix, "");
1713
2347
  let expected = policy.bun;
1714
2348
  if (tool === "node") expected = policy.node;
1715
2349
  else if (tool === "python") expected = policy.python;
2350
+ else if (tool === "rust") expected = policy.rust;
1716
2351
  if (digest !== void 0) {
1717
2352
  if (version === void 0 || !/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/.test(version)) {
1718
2353
  add({
1719
- rule: tool === "oven/bun" ? "bun-pins" : "runtime-docker",
2354
+ rule: imageRule,
1720
2355
  line,
1721
2356
  message: `${label} ${tool} digest requires an exact stable patch tag matching ${expected}`
1722
2357
  });
@@ -1734,7 +2369,7 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
1734
2369
  }
1735
2370
  }
1736
2371
  pin({
1737
- rule: tool === "oven/bun" ? "bun-pins" : "runtime-docker",
2372
+ rule: imageRule,
1738
2373
  value: version,
1739
2374
  expected,
1740
2375
  selector: tool === "node" ? "node" : void 0,
@@ -1751,6 +2386,17 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
1751
2386
  const heredocs = [];
1752
2387
  let instruction = "";
1753
2388
  let startLine = 1;
2389
+ let pendingRustRun;
2390
+ const checkRustCommand = (source, line) => {
2391
+ diagnostics.push(...checkRustReferences({
2392
+ type: "command",
2393
+ file,
2394
+ source,
2395
+ line,
2396
+ rust: policy.rust,
2397
+ exceptions: rustExceptions
2398
+ }));
2399
+ };
1754
2400
  const expand = (value) => {
1755
2401
  for (let count = 0; count <= variables.size; count++) {
1756
2402
  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 +2483,49 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
1837
2483
  if (key !== void 0 && value !== void 0) variables.set(key, expand(value.replace(/^(["'])(.*)\1$/, "$2")));
1838
2484
  return;
1839
2485
  }
1840
- if (/^\s*(?:RUN|COPY)\s+/i.test(content)) {
2486
+ const run = /^\s*RUN\s+(.+)$/i.exec(content);
2487
+ if (run !== null) {
2488
+ let source = run[1] ?? "";
2489
+ source = source.replace(/^(?:--\S+\s+)*/, "");
2490
+ if (source.startsWith("[")) try {
2491
+ const words = JSON.parse(source);
2492
+ if (Array.isArray(words) && words.every((word) => typeof word === "string")) source = words.map((word) => `'${word.replaceAll("'", "'\\''")}'`).join(" ");
2493
+ } catch {}
2494
+ const implicit = /^(<<-?(?:'[^']+'|"[^"]+"|\S+))(?:\s+(.+))?$/.exec(source);
2495
+ if (implicit !== null && implicit[2] === void 0) source = `sh ${source}`;
2496
+ collectHeredocs(content, line);
2497
+ if (heredocs.length > 0) pendingRustRun = {
2498
+ source,
2499
+ line
2500
+ };
2501
+ else checkRustCommand(source, line);
2502
+ return;
2503
+ }
2504
+ const environment = /^\s*ENV\s+(.+)$/i.exec(content);
2505
+ if (environment !== null) {
2506
+ const assignments = dockerEnvironmentAssignments({
2507
+ source: environment[1] ?? "",
2508
+ escape
2509
+ });
2510
+ if (assignments === void 0) {
2511
+ add({
2512
+ rule: "runtime-docker",
2513
+ line,
2514
+ message: "invalid Docker ENV assignments"
2515
+ });
2516
+ return;
2517
+ }
2518
+ diagnostics.push(...checkRustReferences({
2519
+ type: "environment",
2520
+ file,
2521
+ assignments,
2522
+ line,
2523
+ rust: policy.rust,
2524
+ exceptions: rustExceptions
2525
+ }));
2526
+ return;
2527
+ }
2528
+ if (/^\s*COPY\s+/i.test(content)) {
1841
2529
  collectHeredocs(content, line);
1842
2530
  return;
1843
2531
  }
@@ -1858,7 +2546,12 @@ const checkRuntimeFile = ({ file, text, policy, trackedFiles, readFile, reposito
1858
2546
  lines.forEach((line, index) => {
1859
2547
  const heredoc = heredocs.at(0);
1860
2548
  if (heredoc !== void 0) {
2549
+ if (pendingRustRun !== void 0) pendingRustRun.source += `\n${line}`;
1861
2550
  if ((heredoc.stripTabs ? line.replace(/^\t+/, "") : line) === heredoc.delimiter) heredocs.shift();
2551
+ if (heredocs.length === 0 && pendingRustRun !== void 0) {
2552
+ checkRustCommand(pendingRustRun.source, pendingRustRun.line);
2553
+ pendingRustRun = void 0;
2554
+ }
1862
2555
  return;
1863
2556
  }
1864
2557
  if (line.trimStart().startsWith("#")) return;
@@ -2533,18 +3226,20 @@ const record = (value) => typeof value === "object" && value !== null && !Array.
2533
3226
  const array = (value) => Array.isArray(value);
2534
3227
  /** Repository-owned declarations are explicit; malformed decisions fail closed. */
2535
3228
  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");
3229
+ 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
3230
  const disabled = /* @__PURE__ */ new Set();
2538
3231
  const optOuts = input["optOuts"] ?? [];
2539
3232
  if (!array(optOuts)) throw new Error("optOuts must be an array");
2540
3233
  for (const entry of optOuts) {
2541
3234
  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");
3235
+ if (entry["rule"] === "rust-version") throw new Error("rust-version requires expiring exact-pin rustExceptions, not an opt-out");
2542
3236
  if (entry["rule"] === "node-engine") throw new Error("node-engine cannot be opted out; engines.node must support the shared Node series");
2543
3237
  if (entry["rule"] === "cloud-setup-drift") throw new Error("cloud-setup-drift cannot be opted out; generated setup must match shared policy");
2544
3238
  disabled.add(entry["rule"]);
2545
3239
  }
2546
3240
  return {
2547
3241
  disabled,
3242
+ rustExceptions: parseRustExceptions(input["rustExceptions"]),
2548
3243
  cloud: require_toolchain_inputs.parseCloudSetup(input["cloud"]),
2549
3244
  engineFloors: parseEngineFloors(input["engineFloors"]),
2550
3245
  dynamicSelectors: parseDynamicSelectors(input["dynamicSelectors"]),
@@ -2557,27 +3252,47 @@ const readToolchainInputs = (root) => {
2557
3252
  const tracked = (0, node_child_process.execFileSync)("git", [
2558
3253
  "ls-files",
2559
3254
  "-z",
2560
- "--cached"
3255
+ "--cached",
3256
+ "--stage"
2561
3257
  ], {
2562
3258
  cwd: root,
2563
3259
  encoding: "utf8"
2564
- }).split("\0").filter((file) => file !== "");
3260
+ }).split("\0").filter((entry) => entry !== "" && !entry.startsWith("160000 ")).map((entry) => entry.slice(entry.indexOf(" ") + 1));
2565
3261
  const files = {};
2566
3262
  const trackedFiles = new Set(tracked);
2567
3263
  const resolvedRoot = (0, node_fs.realpathSync)(root);
2568
3264
  for (const file of tracked) {
2569
3265
  const kind = require_toolchain_inputs.toolchainInputKind(file);
2570
- if (kind === void 0) continue;
3266
+ const shebangCandidate = kind === void 0 && node_path.default.posix.extname(file) === "" && !file.split("/").some((part) => part === "vendor" || part === "node_modules");
3267
+ if (kind === void 0 && !shebangCandidate) continue;
2571
3268
  if (kind === "presence") {
2572
3269
  files[file] = "";
2573
3270
  continue;
2574
3271
  }
2575
3272
  try {
2576
- const resolvedFile = (0, node_fs.realpathSync)(node_path.default.join(resolvedRoot, file));
3273
+ const inputPath = node_path.default.join(resolvedRoot, file);
3274
+ if (shebangCandidate) {
3275
+ const input = (0, node_fs.lstatSync)(inputPath);
3276
+ if (!input.isFile() && !input.isSymbolicLink()) continue;
3277
+ }
3278
+ const resolvedFile = (0, node_fs.realpathSync)(inputPath);
2577
3279
  const relative = node_path.default.relative(resolvedRoot, resolvedFile);
2578
3280
  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");
3281
+ if (shebangCandidate) {
3282
+ if (!(0, node_fs.lstatSync)(resolvedFile).isFile()) continue;
3283
+ const descriptor = (0, node_fs.openSync)(resolvedFile, "r");
3284
+ const prefix = Buffer.alloc(512);
3285
+ let count;
3286
+ try {
3287
+ count = (0, node_fs.readSync)(descriptor, prefix, 0, prefix.length, 0);
3288
+ } finally {
3289
+ (0, node_fs.closeSync)(descriptor);
3290
+ }
3291
+ if (require_toolchain_inputs.toolchainInputKind(file, prefix.toString("utf8", 0, count)) === void 0) continue;
3292
+ }
2579
3293
  files[file] = (0, node_fs.readFileSync)(resolvedFile, "utf8");
2580
3294
  } catch {
3295
+ if (shebangCandidate) continue;
2581
3296
  diagnostics.push({
2582
3297
  rule: "configuration",
2583
3298
  path: file,
@@ -2678,6 +3393,7 @@ const checkToolchain = ({ root, policy, onDelegated }) => {
2678
3393
  }
2679
3394
  },
2680
3395
  trackedFiles,
3396
+ rustExceptions: configuration.rustExceptions,
2681
3397
  dynamicSelectors: dynamicSelectors.filter((entry) => entry.path === file),
2682
3398
  onDynamicSelector: (entry) => selectorMatches.set(entry, (selectorMatches.get(entry) ?? 0) + 1),
2683
3399
  repository,