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