@stll/oxlint-config 1.8.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +99 -67
  2. package/dist/cloud-setup-cli.cjs +5 -1
  3. package/dist/cloud-setup-cli.cjs.map +1 -1
  4. package/dist/cloud-setup-cli.mjs +6 -2
  5. package/dist/cloud-setup-cli.mjs.map +1 -1
  6. package/dist/dependabot.d.cts +0 -19
  7. package/dist/dependabot.d.mts +0 -19
  8. package/dist/git-tracked-output.cjs +1 -1
  9. package/dist/git-tracked-output.mjs +1 -1
  10. package/dist/publish-artifact-syntax.cjs +1 -1
  11. package/dist/publish-artifact-syntax.mjs +1 -1
  12. package/dist/publish-contract-cli.cjs +2 -2
  13. package/dist/publish-contract-cli.mjs +2 -2
  14. package/dist/quarantine-cli.cjs +3 -2
  15. package/dist/quarantine-cli.cjs.map +1 -1
  16. package/dist/quarantine-cli.mjs +3 -2
  17. package/dist/quarantine-cli.mjs.map +1 -1
  18. package/dist/quarantine-policy.d.cts +25 -2
  19. package/dist/quarantine-policy.d.mts +25 -2
  20. package/dist/quarantine-policy2.cjs +177 -5
  21. package/dist/quarantine-policy2.cjs.map +1 -1
  22. package/dist/quarantine-policy2.mjs +166 -6
  23. package/dist/quarantine-policy2.mjs.map +1 -1
  24. package/dist/toolchain-changed-cli.cjs +1477 -2
  25. package/dist/toolchain-changed-cli.cjs.map +1 -1
  26. package/dist/toolchain-changed-cli.mjs +1474 -1
  27. package/dist/toolchain-changed-cli.mjs.map +1 -1
  28. package/dist/toolchain-check-cli.cjs +2 -0
  29. package/dist/toolchain-check-cli.cjs.map +1 -1
  30. package/dist/toolchain-check-cli.mjs +2 -0
  31. package/dist/toolchain-check-cli.mjs.map +1 -1
  32. package/dist/toolchain-guard.cjs +355 -241
  33. package/dist/toolchain-guard.cjs.map +1 -1
  34. package/dist/toolchain-guard.mjs +352 -244
  35. package/dist/toolchain-guard.mjs.map +1 -1
  36. package/dist/toolchain-inputs.cjs +13 -40
  37. package/dist/toolchain-inputs.cjs.map +1 -1
  38. package/dist/toolchain-inputs.mjs +13 -40
  39. package/dist/toolchain-inputs.mjs.map +1 -1
  40. package/dist/toolchain-schema.cjs +69 -46
  41. package/dist/toolchain-schema.cjs.map +1 -1
  42. package/dist/toolchain-schema.mjs +69 -47
  43. package/dist/toolchain-schema.mjs.map +1 -1
  44. package/dist/typecheck-probe-cli.cjs +40 -2
  45. package/dist/typecheck-probe-cli.cjs.map +1 -1
  46. package/dist/typecheck-probe-cli.mjs +39 -1
  47. package/dist/typecheck-probe-cli.mjs.map +1 -1
  48. package/package.json +3 -4
  49. package/toolchain.json +2 -28
  50. package/dist/toolchain-changed.cjs +0 -1484
  51. package/dist/toolchain-changed.cjs.map +0 -1
  52. package/dist/toolchain-changed.mjs +0 -1477
  53. package/dist/toolchain-changed.mjs.map +0 -1
  54. package/dist/typecheck-parity-cli.cjs +0 -71
  55. package/dist/typecheck-parity-cli.cjs.map +0 -1
  56. package/dist/typecheck-parity-cli.d.cts +0 -1
  57. package/dist/typecheck-parity-cli.d.mts +0 -1
  58. package/dist/typecheck-parity-cli.mjs +0 -71
  59. package/dist/typecheck-parity-cli.mjs.map +0 -1
  60. package/dist/typecheck-parity.cjs +0 -907
  61. package/dist/typecheck-parity.cjs.map +0 -1
  62. package/dist/typecheck-parity.mjs +0 -896
  63. package/dist/typecheck-parity.mjs.map +0 -1
@@ -7,17 +7,34 @@ const require_toolchain_dependabot = require("./toolchain-dependabot.cjs");
7
7
  const require_toolchain_schema = require("./toolchain-schema.cjs");
8
8
  let node_path = require("node:path");
9
9
  node_path = require_rolldown_runtime.__toESM(node_path, 1);
10
+ let node_child_process = require("node:child_process");
10
11
  let node_fs = require("node:fs");
11
12
  let smol_toml = require("smol-toml");
12
- let node_child_process = require("node:child_process");
13
13
  let semver = require("semver");
14
14
  let yaml = require("yaml");
15
15
  let picomatch = require("picomatch");
16
16
  picomatch = require_rolldown_runtime.__toESM(picomatch, 1);
17
17
  let node_module = require("node:module");
18
+ let node_url = require("node:url");
19
+ //#region src/toolchain-bun.ts
20
+ const STABLE_RELEASE = /^\d+\.\d+\.\d+$/u;
21
+ /**
22
+ * The shared policy names the minimum Bun. A repository pins one exact
23
+ * release at or above it in its root packageManager, and every other Bun
24
+ * reference in the repository must equal that release.
25
+ */
26
+ const repositoryBun = (floor, manifest) => {
27
+ let packageManager;
28
+ try {
29
+ const parsed = JSON.parse(manifest ?? "");
30
+ if (typeof parsed === "object" && parsed !== null) packageManager = Reflect.get(parsed, "packageManager");
31
+ } catch {}
32
+ const version = typeof packageManager === "string" ? /^bun@(.+)$/.exec(packageManager)?.[1] : void 0;
33
+ return version !== void 0 && STABLE_RELEASE.test(version) && (0, semver.valid)(version) === version && (0, semver.gte)(version, floor) ? version : floor;
34
+ };
18
35
  //#endregion
19
36
  //#region src/toolchain-consumer-checks.ts
20
- const consumerRunnerVersion = "1.8.0";
37
+ const consumerRunnerVersion = "2.0.0";
21
38
  const consumerCheckWorkflow = "stella/.github/.github/workflows/package-consumer-compat.yml";
22
39
  const record$10 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
23
40
  const repositoryDirectory = (value) => typeof value === "string" && value !== "" && !value.includes("\\") && !value.includes("${{") && !value.split("").some((character) => {
@@ -352,7 +369,71 @@ const packageRules = [
352
369
  "typescript-layout",
353
370
  "node-engine"
354
371
  ];
372
+ /** Compiler packages `bun check` replaces; an install needs a repository allowlist entry. */
373
+ const typescriptPackages = /* @__PURE__ */ new Set(["typescript", "@typescript/native"]);
374
+ const typecheckCommand = "bun check";
375
+ const typescriptGuidance = "type check with `bun check --no-pretty --all --project=<tsconfig>`, use tsrs for programs and type checkers, and oxc-parser for syntax-only reads";
376
+ const installSections = [
377
+ "dependencies",
378
+ "devDependencies",
379
+ "optionalDependencies"
380
+ ];
381
+ const allowlistFile = "stll-toolchain.json";
355
382
  const record$7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
383
+ /** `npm:typescript@7.0.2` installs a compiler under another name; returns its version. */
384
+ const typescriptAliasVersion = (spec) => {
385
+ if (typeof spec !== "string") return void 0;
386
+ const match = /^npm:(?:typescript|@typescript\/native)(?:@(.*))?$/.exec(spec);
387
+ return match === null ? void 0 : match[1] ?? "";
388
+ };
389
+ const exactTypescriptSpec = (spec) => {
390
+ if (typeof spec !== "string") return false;
391
+ const version = typescriptAliasVersion(spec) ?? spec;
392
+ return (0, semver.valid)(version) === version;
393
+ };
394
+ const isTypescriptInstall = (name, value) => typescriptPackages.has(name) || typescriptAliasVersion(value) !== void 0;
395
+ const allowlistKeys = [
396
+ "workspace",
397
+ "name",
398
+ "spec",
399
+ "dependency",
400
+ "reason"
401
+ ];
402
+ const allowlistFormat = "{\"workspace\": \"<package directory or .>\", \"name\": \"typescript\", \"spec\": \"<exact version>\", \"dependency\": \"<package that needs it>\", \"reason\": \"<why>\"}";
403
+ const workspaceManifest = (workspace) => workspace === "." ? "package.json" : `${workspace}/package.json`;
404
+ /** Each repository declares its own TypeScript installs; there is no shared list. */
405
+ const parseTypescriptAllowlist = (input) => {
406
+ if (input === void 0) return [];
407
+ if (!Array.isArray(input)) throw new Error(`typescriptAllowlist must be an array of entries like ${allowlistFormat}`);
408
+ const seen = /* @__PURE__ */ new Set();
409
+ return input.map((entry) => {
410
+ const shown = JSON.stringify(entry);
411
+ const text = (key) => {
412
+ const value = record$7(entry) ? entry[key] : void 0;
413
+ return typeof value === "string" && value.trim() !== "" ? value : void 0;
414
+ };
415
+ const workspace = text("workspace");
416
+ const name = text("name");
417
+ const spec = text("spec");
418
+ const dependency = text("dependency");
419
+ const reason = text("reason");
420
+ if (!record$7(entry) || Object.keys(entry).length !== allowlistKeys.length || workspace === void 0 || name === void 0 || spec === void 0 || dependency === void 0 || reason === void 0) throw new Error(`typescriptAllowlist entry ${shown} must have exactly workspace, name, spec, dependency and reason as nonempty strings: ${allowlistFormat}`);
421
+ if (workspace !== "." && (node_path.default.posix.normalize(workspace) !== workspace || workspace.startsWith("/") || workspace.endsWith("/") || workspace === ".." || workspace.startsWith("../"))) throw new Error(`typescriptAllowlist entry ${shown}: workspace must be "." for the root or a normalized relative package directory such as "apps/web"`);
422
+ if (!exactTypescriptSpec(spec)) throw new Error(`typescriptAllowlist entry ${shown}: spec must be the exact spec the manifest resolves to, such as "6.0.3" or "npm:typescript@6.0.3", not a range or catalog reference`);
423
+ if (!isTypescriptInstall(name, spec)) throw new Error(`typescriptAllowlist entry ${shown}: name must be typescript or @typescript/native, or an alias whose spec is npm:typescript@<version> or npm:@typescript/native@<version>`);
424
+ if (typescriptPackages.has(dependency) || dependency === name) throw new Error(`typescriptAllowlist entry ${shown}: dependency must name the package in that workspace that needs ${name}, not a TypeScript package`);
425
+ const key = `${workspace}\0${name}`;
426
+ if (seen.has(key)) throw new Error(`typescriptAllowlist declares ${name} for ${workspace} more than once; keep one entry`);
427
+ seen.add(key);
428
+ return {
429
+ workspace,
430
+ name,
431
+ spec,
432
+ dependency,
433
+ reason
434
+ };
435
+ });
436
+ };
356
437
  const shellSubstitutions = (command) => {
357
438
  const frames = [{
358
439
  kind: "root",
@@ -878,7 +959,7 @@ const invokesCompiler = (words) => {
878
959
  return tokens.slice(start).some((word, index) => compiler(word) || word === "bun" && tokens.at(skipOptions(start + index + 1, "bun")) === "check");
879
960
  };
880
961
  /** Validate manifests and catalog resolutions from a tracked-file snapshot. */
881
- const checkPackageFiles = ({ files, policy }) => {
962
+ const checkPackageFiles = ({ files, policy, typescriptAllowlist }) => {
882
963
  const sources = new Map(Object.entries(files));
883
964
  const diagnostics = [];
884
965
  const pins = new Map(Object.entries({
@@ -994,16 +1075,29 @@ const checkPackageFiles = ({ files, policy }) => {
994
1075
  directory = node_path.default.posix.dirname(directory);
995
1076
  }
996
1077
  };
997
- const resolve = (file, name, value) => {
1078
+ const resolveThrough = (file, name, value) => {
998
1079
  const visited = /* @__PURE__ */ new Set();
1080
+ const catalogs = [];
999
1081
  while (typeof value === "string" && value.startsWith("catalog:")) {
1000
- if (visited.has(value)) return void 0;
1082
+ if (visited.has(value)) return {
1083
+ value: void 0,
1084
+ catalogs
1085
+ };
1001
1086
  visited.add(value);
1002
1087
  const catalog = catalogFor(file, value.slice(8));
1003
- value = record$7(catalog) ? catalog[name] : void 0;
1088
+ if (!record$7(catalog)) return {
1089
+ value: void 0,
1090
+ catalogs
1091
+ };
1092
+ catalogs.push(catalog);
1093
+ value = catalog[name];
1004
1094
  }
1005
- return value;
1095
+ return {
1096
+ value,
1097
+ catalogs
1098
+ };
1006
1099
  };
1100
+ const resolve = (file, name, value) => resolveThrough(file, name, value).value;
1007
1101
  const workspaceOwnerFor = (file) => {
1008
1102
  const pnpm = pnpmWorkspaceFor(file);
1009
1103
  if (pnpm !== void 0) return {
@@ -1042,88 +1136,49 @@ const checkPackageFiles = ({ files, policy }) => {
1042
1136
  }));
1043
1137
  return candidates.length === 1 && candidates.every(([, json]) => json["version"] === pin) ? "matched" : "mismatch";
1044
1138
  };
1045
- const tsSpecifiers = /* @__PURE__ */ new Map();
1046
- for (const layout of policy.typescriptInstallLayouts) {
1047
- const values = tsSpecifiers.get(layout.compilerPackage) ?? /* @__PURE__ */ new Set();
1048
- values.add(layout.compilerSpecifier);
1049
- tsSpecifiers.set(layout.compilerPackage, values);
1050
- if (layout.type !== "split-compatibility") continue;
1051
- const compatibility = tsSpecifiers.get(layout.compatibilityPackage) ?? /* @__PURE__ */ new Set();
1052
- compatibility.add(layout.compatibilitySpecifier);
1053
- tsSpecifiers.set(layout.compatibilityPackage, compatibility);
1054
- }
1055
- tsSpecifiers.set(policy.typescript6Compatibility.packageAlias, /* @__PURE__ */ new Set([`npm:typescript@${policy.typescript6Compatibility.version}`]));
1056
- const layoutFor = (specifiers) => policy.typescriptInstallLayouts.find((layout) => {
1057
- const compiler = specifiers.get(layout.compilerPackage);
1058
- if (compiler?.size !== 1 || !compiler.has(layout.compilerSpecifier)) return false;
1059
- if (layout.type === "split-compatibility") {
1060
- const compatibility = specifiers.get(layout.compatibilityPackage);
1061
- return compatibility?.size === 1 && compatibility.has(layout.compatibilitySpecifier);
1062
- }
1063
- return policy.typescriptInstallLayouts.every((other) => other.compilerPackage === layout.compilerPackage || !specifiers.has(other.compilerPackage));
1064
- });
1065
- const peerRange = (value) => typeof value === "string" && (0, semver.valid)(value) === null && (0, semver.validRange)(value) !== null;
1066
- const toolchainEntries = (file, json) => {
1067
- const entries = [];
1068
- for (const section of [
1069
- "dependencies",
1070
- "devDependencies",
1071
- "optionalDependencies",
1072
- "peerDependencies"
1073
- ]) {
1074
- const dependencies = json[section];
1075
- if (!record$7(dependencies)) continue;
1076
- for (const [name, value] of Object.entries(dependencies)) {
1077
- if (!tsSpecifiers.has(name)) continue;
1078
- const resolved = resolve(file, name, value);
1079
- if (section === "peerDependencies" && peerRange(resolved)) continue;
1080
- entries.push({
1081
- section,
1082
- name,
1083
- value: resolved
1084
- });
1139
+ const allowed = new Map(typescriptAllowlist.map((entry) => [`${workspaceManifest(entry.workspace)}\0${entry.name}`, entry]));
1140
+ const allowedCatalogEntries = /* @__PURE__ */ new Map();
1141
+ for (const entry of typescriptAllowlist) {
1142
+ const file = workspaceManifest(entry.workspace);
1143
+ const json = manifests.get(file);
1144
+ if (json === void 0) continue;
1145
+ for (const section of installSections) {
1146
+ const entries = json[section];
1147
+ if (!record$7(entries) || !Object.hasOwn(entries, entry.name)) continue;
1148
+ const { value, catalogs } = resolveThrough(file, entry.name, entries[entry.name]);
1149
+ if (value !== entry.spec) continue;
1150
+ for (const catalog of catalogs) {
1151
+ const names = allowedCatalogEntries.get(catalog) ?? /* @__PURE__ */ new Set();
1152
+ names.add(entry.name);
1153
+ allowedCatalogEntries.set(catalog, names);
1085
1154
  }
1086
1155
  }
1087
- return entries;
1088
- };
1089
- const specifierMap = (entries) => {
1090
- const specifiers = /* @__PURE__ */ new Map();
1091
- for (const { name, value } of entries) {
1092
- const values = specifiers.get(name) ?? /* @__PURE__ */ new Set();
1093
- values.add(value);
1094
- specifiers.set(name, values);
1095
- }
1096
- return specifiers;
1156
+ }
1157
+ const shownSpec = (raw, value) => {
1158
+ if (typeof value !== "string") return `${String(raw)} (unresolved)`;
1159
+ return raw === value ? value : `${value} (from ${String(raw)})`;
1097
1160
  };
1098
- const workspaceLayouts = /* @__PURE__ */ new Map();
1099
- const workspaceLayoutFor = (file) => {
1100
- const owner = workspaceOwnerFor(file);
1101
- if (owner === void 0 || !require_toolchain_inputs.workspaceContains({
1102
- ...owner,
1103
- file,
1104
- rootMembership: "implicit"
1105
- })) return void 0;
1106
- const key = `${owner.source}:${owner.directory}`;
1107
- const cached = workspaceLayouts.get(key);
1108
- if (cached !== void 0) return cached;
1109
- const entries = [];
1110
- for (const [candidate, member] of manifests) {
1111
- if (node_path.default.posix.basename(candidate) !== "package.json") continue;
1112
- const memberOwner = workspaceOwnerFor(candidate);
1113
- if (memberOwner?.directory !== owner.directory || memberOwner.source !== owner.source || !require_toolchain_inputs.workspaceContains({
1114
- ...owner,
1115
- file: candidate,
1116
- rootMembership: "implicit"
1117
- })) continue;
1118
- entries.push(...toolchainEntries(candidate, member).filter(({ section }) => section === "devDependencies"));
1161
+ const typescriptMessage = (name, value) => `${name} ${String(value)} is not needed: ${typescriptGuidance}`;
1162
+ const typescriptEntryMessage = ({ file, entries, name, raw, value, scope }) => {
1163
+ const shown = shownSpec(raw, value);
1164
+ if (scope === "catalog") return allowedCatalogEntries.get(entries)?.has(name) === true ? void 0 : `${name} ${shown} in a catalog of ${file} serves no allowed install: ${typescriptGuidance}; a catalog may hold ${name} only at the spec of a workspace entry in typescriptAllowlist in ${allowlistFile}`;
1165
+ if (scope === "resolution") return `${name} must not be overridden or resolved (${shown}): ${typescriptGuidance}; declare it in the workspace whose dependency needs it and add a typescriptAllowlist entry in ${allowlistFile}`;
1166
+ if (scope === "peer") return `${name} peer ${shown} is not a version range: ${typescriptGuidance}`;
1167
+ const entry = node_path.default.posix.basename(file) === "package.json" ? allowed.get(`${file}\0${name}`) : void 0;
1168
+ if (entry === void 0) {
1169
+ const workspace = node_path.default.posix.dirname(file);
1170
+ const suggestion = JSON.stringify({
1171
+ workspace,
1172
+ name,
1173
+ spec: exactTypescriptSpec(value) ? value : "<exact version>",
1174
+ dependency: "<package that needs it>",
1175
+ reason: "<why it needs this TypeScript>"
1176
+ });
1177
+ return `${name} ${shown} in ${file} is not allowed: ${typescriptGuidance}; if a dependency of this workspace needs it, add ${suggestion} to typescriptAllowlist in ${allowlistFile}`;
1119
1178
  }
1120
- const result = {
1121
- layout: layoutFor(specifierMap(entries)),
1122
- directory: owner.directory
1123
- };
1124
- workspaceLayouts.set(key, result);
1125
- return result;
1179
+ return value === entry.spec ? void 0 : `${name} in ${file} resolves to ${shown}, but typescriptAllowlist in ${allowlistFile} allows ${entry.spec} for ${entry.dependency}; declare ${entry.spec} or update the entry's spec`;
1126
1180
  };
1181
+ const peerRange = (value) => typeof value === "string" && (0, semver.validRange)(value) !== null;
1127
1182
  for (const [file, json] of manifests) {
1128
1183
  const manager = json["packageManager"];
1129
1184
  if (manager !== void 0 && manager !== `bun@${policy.bun}`) add({
@@ -1131,20 +1186,27 @@ const checkPackageFiles = ({ files, policy }) => {
1131
1186
  rule: "bun-pins",
1132
1187
  key: "packageManager",
1133
1188
  value: manager,
1134
- message: `packageManager Bun version must be ${policy.bun}, found ${JSON.stringify(manager)}`
1189
+ message: `packageManager Bun version must be ${policy.bun} (one exact release at or above the shared minimum, the same in every manifest), found ${JSON.stringify(manager)}`
1135
1190
  });
1136
- const checkEntries = (entries, location) => {
1191
+ const checkEntries = (entries, scope, location) => {
1137
1192
  if (!record$7(entries)) return;
1138
1193
  for (const [name, raw] of Object.entries(entries)) {
1139
1194
  const value = resolve(file, name, raw);
1140
- const tsAllowed = tsSpecifiers.get(name);
1141
- if (tsAllowed !== void 0) {
1142
- if (typeof value !== "string" || !tsAllowed.has(value)) add({
1195
+ if (isTypescriptInstall(name, value)) {
1196
+ const message = typescriptEntryMessage({
1197
+ file,
1198
+ entries,
1199
+ name,
1200
+ raw,
1201
+ value,
1202
+ scope
1203
+ });
1204
+ if (message !== void 0) add({
1143
1205
  file,
1144
1206
  rule: "typescript-layout",
1145
1207
  key: location?.key ?? name,
1146
1208
  value: location === void 0 ? raw : location.value,
1147
- message: `${name} must use a declared TypeScript specifier, found ${String(value)}`
1209
+ message
1148
1210
  });
1149
1211
  continue;
1150
1212
  }
@@ -1166,20 +1228,17 @@ const checkPackageFiles = ({ files, policy }) => {
1166
1228
  });
1167
1229
  }
1168
1230
  };
1169
- for (const key of [
1170
- "dependencies",
1171
- "devDependencies",
1172
- "optionalDependencies"
1173
- ]) checkEntries(json[key]);
1174
- if (record$7(json["peerDependencies"])) {
1175
- for (const [name, value] of Object.entries(json["peerDependencies"])) if (tsSpecifiers.has(name) && !peerRange(resolve(file, name, value))) checkEntries({ [name]: value });
1231
+ for (const key of installSections) checkEntries(json[key], "install");
1232
+ if (record$7(json["peerDependencies"])) for (const [name, value] of Object.entries(json["peerDependencies"])) {
1233
+ const resolved = resolve(file, name, value);
1234
+ if (isTypescriptInstall(name, resolved) && !peerRange(resolved)) checkEntries({ [name]: value }, "peer");
1176
1235
  }
1177
1236
  for (const source of catalogSources(json)) {
1178
- checkEntries(source["catalog"]);
1237
+ checkEntries(source["catalog"], "catalog");
1179
1238
  if (!record$7(source["catalogs"])) continue;
1180
- for (const catalog of Object.values(source["catalogs"])) checkEntries(catalog);
1239
+ for (const catalog of Object.values(source["catalogs"])) checkEntries(catalog, "catalog");
1181
1240
  }
1182
- const ownedNames = /* @__PURE__ */ new Set([...pins.keys(), ...tsSpecifiers.keys()]);
1241
+ const ownedNames = /* @__PURE__ */ new Set([...pins.keys(), ...typescriptPackages]);
1183
1242
  const resolutionDependencies = {};
1184
1243
  const resolutionRoot = manifests.get(node_path.default.posix.join(node_path.default.posix.dirname(file), "package.json"));
1185
1244
  if (resolutionRoot !== void 0) for (const section of [
@@ -1191,32 +1250,6 @@ const checkPackageFiles = ({ files, policy }) => {
1191
1250
  if (!record$7(entries)) continue;
1192
1251
  for (const [name, value] of Object.entries(entries)) resolutionDependencies[name] = resolve(file, name, value);
1193
1252
  }
1194
- const affectedSpecifiers = /* @__PURE__ */ new Map();
1195
- const owner = workspaceOwnerFor(file);
1196
- for (const [candidate, member] of manifests) {
1197
- if (node_path.default.posix.basename(candidate) !== "package.json") continue;
1198
- const memberOwner = workspaceOwnerFor(candidate);
1199
- if (!(candidate === node_path.default.posix.join(node_path.default.posix.dirname(file), "package.json")) && !(owner !== void 0 && owner.directory === node_path.default.posix.dirname(file) && memberOwner?.directory === owner.directory && memberOwner.source === owner.source && require_toolchain_inputs.workspaceContains({
1200
- directory: owner.directory,
1201
- file: candidate,
1202
- patterns: owner.patterns,
1203
- rootMembership: "implicit"
1204
- }))) continue;
1205
- for (const section of [
1206
- "dependencies",
1207
- "devDependencies",
1208
- "optionalDependencies"
1209
- ]) {
1210
- const entries = member[section];
1211
- if (!record$7(entries)) continue;
1212
- for (const [name, value] of Object.entries(entries)) {
1213
- if (!tsSpecifiers.has(name)) continue;
1214
- const values = affectedSpecifiers.get(name) ?? /* @__PURE__ */ new Set();
1215
- values.add(resolve(candidate, name, value));
1216
- affectedSpecifiers.set(name, values);
1217
- }
1218
- }
1219
- }
1220
1253
  const checkResolutionMap = (entries, parent) => {
1221
1254
  if (!record$7(entries)) return;
1222
1255
  for (const [selector, replacement] of Object.entries(entries)) {
@@ -1224,11 +1257,7 @@ const checkPackageFiles = ({ files, policy }) => {
1224
1257
  if (target === void 0) continue;
1225
1258
  let pattern = target.split(/>\s*(?=[@A-Za-z_*]|$)/).at(-1)?.trim() ?? "";
1226
1259
  const version = pattern.lastIndexOf("@");
1227
- let qualifier;
1228
- if (version > pattern.lastIndexOf("/")) {
1229
- qualifier = pattern.slice(version + 1).replace(/^npm:/, "");
1230
- pattern = pattern.slice(0, version);
1231
- }
1260
+ if (version > pattern.lastIndexOf("/")) pattern = pattern.slice(0, version);
1232
1261
  const parts = pattern.split("/");
1233
1262
  pattern = parts.at(-2)?.startsWith("@") ? parts.slice(-2).join("/") : parts.at(-1) ?? "";
1234
1263
  if (pattern === "") {
@@ -1245,102 +1274,60 @@ const checkPackageFiles = ({ files, policy }) => {
1245
1274
  checkResolutionMap(replacement, target);
1246
1275
  continue;
1247
1276
  }
1248
- for (const name of ownedNames) {
1249
- if (!(0, picomatch.default)(pattern)(name)) continue;
1250
- let value = replacement;
1251
- if (typeof replacement === "string" && replacement.startsWith("$")) {
1252
- const reference = replacement.slice(1);
1253
- value = resolutionDependencies[reference];
1254
- }
1255
- const resolved = resolve(file, name, value);
1256
- checkEntries({ [name]: value }, {
1257
- key: selector,
1258
- value: replacement
1259
- });
1260
- if (tsSpecifiers.has(name) && [...affectedSpecifiers.get(name) ?? []].some((specifier) => {
1261
- if (resolved === specifier) return false;
1262
- if (qualifier === void 0 || (0, semver.validRange)(qualifier) === null) return true;
1263
- if (typeof specifier !== "string") return false;
1264
- const versionSpecifier = specifier.startsWith("npm:") ? specifier.slice(specifier.lastIndexOf("@") + 1) : specifier;
1265
- const release = (0, semver.valid)(versionSpecifier);
1266
- return release !== null && (0, semver.satisfies)(release, qualifier);
1267
- }) && typeof resolved === "string" && tsSpecifiers.get(name)?.has(resolved)) add({
1268
- file,
1269
- rule: "typescript-layout",
1270
- key: selector,
1271
- value: replacement,
1272
- message: `${name} resolution must preserve its declared TypeScript install specifier`
1273
- });
1277
+ let value = replacement;
1278
+ if (typeof replacement === "string" && replacement.startsWith("$")) {
1279
+ const reference = replacement.slice(1);
1280
+ value = resolutionDependencies[reference];
1274
1281
  }
1282
+ const location = {
1283
+ key: selector,
1284
+ value: replacement
1285
+ };
1286
+ const targets = [...ownedNames].filter((name) => (0, picomatch.default)(pattern)(name));
1287
+ for (const name of targets) checkEntries({ [name]: value }, "resolution", location);
1288
+ if (typescriptAliasVersion(value) !== void 0 && !targets.some((name) => isTypescriptInstall(name, value))) checkEntries({ [pattern]: value }, "resolution", location);
1275
1289
  }
1276
1290
  };
1277
1291
  checkResolutionMap(json["overrides"]);
1278
1292
  checkResolutionMap(json["resolutions"]);
1279
1293
  if (record$7(json["pnpm"])) checkResolutionMap(json["pnpm"]["overrides"]);
1280
- const localEntries = toolchainEntries(file, json);
1281
- const usesCompiler = policy.typescriptInstallLayouts.some((layout) => localEntries.some(({ name }) => name === layout.compilerPackage));
1282
- let selectedLayout = layoutFor(specifierMap(localEntries));
1283
- if (usesCompiler) {
1284
- if (layoutFor(specifierMap(localEntries.filter(({ section }) => section !== "peerDependencies"))) === void 0) selectedLayout = void 0;
1285
- if (selectedLayout === void 0) {
1286
- const workspace = workspaceLayoutFor(file);
1287
- const layout = workspace?.layout;
1288
- if (workspace !== void 0 && layout !== void 0) {
1289
- const expected = /* @__PURE__ */ new Map([[layout.compilerPackage, layout.compilerSpecifier]]);
1290
- if (layout.type === "split-compatibility") expected.set(layout.compatibilityPackage, layout.compatibilitySpecifier);
1291
- expected.set(policy.typescript6Compatibility.packageAlias, `npm:typescript@${policy.typescript6Compatibility.version}`);
1292
- const developmentOnly = localEntries.every(({ section, name, value }) => section === "devDependencies" && expected.get(name) === value);
1293
- const runtimeCompatibilityOnly = layout.type === "split-compatibility" && node_path.default.posix.dirname(file) !== workspace.directory && localEntries.every(({ section, name, value }) => (section === "dependencies" || section === "devDependencies" || section === "peerDependencies") && name === layout.compatibilityPackage && value === layout.compatibilitySpecifier);
1294
- if (developmentOnly || runtimeCompatibilityOnly) selectedLayout = layout;
1295
- }
1296
- }
1297
- if (selectedLayout === void 0) add({
1294
+ const command = record$7(json["scripts"]) ? json["scripts"]["typecheck"] : void 0;
1295
+ if (typeof command === "string") for (const words of compilerCommandSegments(command)) {
1296
+ if (!invokesCompiler(words)) continue;
1297
+ const normalized = words.join(" ");
1298
+ if (!words.some((word) => word === "(" || word === ")") && shellSubstitutions(normalized).length === 0 && (normalized === typecheckCommand || normalized.startsWith(`${typecheckCommand} `))) continue;
1299
+ add({
1298
1300
  file,
1299
1301
  rule: "typescript-layout",
1300
- key: "dependencies",
1301
- message: "TypeScript compiler and compatibility packages must match one complete declared install layout"
1302
+ key: "typecheck",
1303
+ value: command,
1304
+ message: `typecheck must run \`${typecheckCommand} --no-pretty --all --project=<tsconfig>\`, not a TypeScript compiler package`
1302
1305
  });
1303
- const command = record$7(json["scripts"]) ? json["scripts"]["typecheck"] : void 0;
1304
- if (selectedLayout !== void 0 && typeof command === "string") {
1305
- const normalizedExpected = selectedLayout.typecheckCommand.replace(/\s+/g, " ").trim();
1306
- for (const words of compilerCommandSegments(command)) {
1307
- if (!invokesCompiler(words)) continue;
1308
- const normalized = words.join(" ");
1309
- if (!words.some((word) => word === "(" || word === ")") && shellSubstitutions(normalized).length === 0 && (normalized === normalizedExpected || normalized.startsWith(`${normalizedExpected} `))) continue;
1310
- add({
1311
- file,
1312
- rule: "typescript-layout",
1313
- key: "typecheck",
1314
- value: command,
1315
- message: `typecheck compiler invocation must use ${selectedLayout.typecheckCommand}`
1316
- });
1317
- break;
1318
- }
1319
- }
1306
+ break;
1320
1307
  }
1321
- if (record$7(json["peerDependencies"])) {
1322
- const peerLayout = selectedLayout ?? workspaceLayoutFor(file)?.layout;
1323
- for (const [name, raw] of Object.entries(json["peerDependencies"])) {
1324
- if (!tsSpecifiers.has(name)) continue;
1325
- const range = resolve(file, name, raw);
1326
- if (!peerRange(range)) continue;
1327
- const releases = /* @__PURE__ */ new Set();
1328
- for (const layout of policy.typescriptInstallLayouts) {
1329
- if (layout.compilerPackage !== name && !(layout.type === "split-compatibility" && layout.compatibilityPackage === name)) continue;
1330
- const specifier = layout.compilerSpecifier;
1331
- releases.add(specifier.startsWith("npm:") ? specifier.slice(specifier.lastIndexOf("@") + 1) : specifier);
1332
- }
1333
- if (name === policy.typescript6Compatibility.packageAlias) releases.add(policy.typescript6Compatibility.version);
1334
- if (peerLayout?.type === "split-compatibility" && name === peerLayout.compatibilityPackage) releases.add(peerLayout.compatibilitySpecifier);
1335
- if ([...releases].every((release) => (0, semver.satisfies)(release, range))) continue;
1308
+ if (record$7(json["peerDependencies"])) for (const [name, raw] of Object.entries(json["peerDependencies"])) {
1309
+ if (!typescriptPackages.has(name)) continue;
1310
+ const range = resolve(file, name, raw);
1311
+ if (!peerRange(range)) continue;
1312
+ const release = name === "typescript" ? policy.typescript : void 0;
1313
+ if (release === void 0) {
1336
1314
  add({
1337
1315
  file,
1338
1316
  rule: "typescript-layout",
1339
1317
  key: name,
1340
1318
  value: raw,
1341
- message: `${name} peer support range must include ${[...releases].join(" and ")}, found ${range}`
1319
+ message: typescriptMessage(name, range)
1342
1320
  });
1321
+ continue;
1343
1322
  }
1323
+ if ((0, semver.satisfies)(release, range)) continue;
1324
+ add({
1325
+ file,
1326
+ rule: "typescript-layout",
1327
+ key: name,
1328
+ value: raw,
1329
+ message: `${name} peer support range must include ${release}, found ${range}`
1330
+ });
1344
1331
  }
1345
1332
  if (record$7(json["engines"]) && json["engines"]["node"] !== void 0 && (typeof json["engines"]["node"] !== "string" || !require_toolchain_inputs.nodeSupportRangeMatches(policy.node, json["engines"]["node"]))) add({
1346
1333
  file,
@@ -1350,6 +1337,35 @@ const checkPackageFiles = ({ files, policy }) => {
1350
1337
  message: `engines.node must support ${policy.node}, found ${JSON.stringify(json["engines"]["node"])}`
1351
1338
  });
1352
1339
  }
1340
+ const staleness = (entry) => {
1341
+ const file = workspaceManifest(entry.workspace);
1342
+ const json = manifests.get(file);
1343
+ if (json === void 0) return `${file} is missing or invalid; remove the entry`;
1344
+ const declares = (name) => installSections.some((section) => {
1345
+ const entries = json[section];
1346
+ return record$7(entries) && Object.hasOwn(entries, name);
1347
+ });
1348
+ if (!declares(entry.name)) return `${file} no longer declares ${entry.name} in dependencies, devDependencies or optionalDependencies; remove the entry`;
1349
+ if (!declares(entry.dependency)) return `${file} no longer depends on ${entry.dependency}; remove the entry and the ${entry.name} declaration, or name the dependency that still needs it`;
1350
+ };
1351
+ for (const entry of typescriptAllowlist) {
1352
+ const stale = staleness(entry);
1353
+ if (stale === void 0) continue;
1354
+ const message = `typescriptAllowlist entry ${entry.name} ${entry.spec} for ${entry.workspace} is stale: ${stale}`;
1355
+ if (sources.has(allowlistFile)) add({
1356
+ file: allowlistFile,
1357
+ rule: "typescript-layout",
1358
+ key: "workspace",
1359
+ value: entry.workspace,
1360
+ message
1361
+ });
1362
+ else diagnostics.push({
1363
+ rule: "typescript-layout",
1364
+ path: allowlistFile,
1365
+ line: 1,
1366
+ message
1367
+ });
1368
+ }
1353
1369
  return diagnostics;
1354
1370
  };
1355
1371
  const extensionKinds = new Map(Object.entries({
@@ -1739,6 +1755,64 @@ const readCommand = ({ root, executable, args, runtime }) => {
1739
1755
  if (result.status !== 0) throw new Error(`configuration discovery failed: ${(result.stderr + result.stdout).trim()}`);
1740
1756
  return result.stdout;
1741
1757
  };
1758
+ const projectConfigScript = `
1759
+ const [moduleUrl, root, config] = process.argv.slice(1);
1760
+ const { API } = await import(moduleUrl);
1761
+ const api = new API({ cwd: root });
1762
+ try {
1763
+ const parsed = await api.parseConfigFile(config);
1764
+ // An empty files list (TS18002) or no inputs (TS18003) leaves a project
1765
+ // without roots; only unreadable configuration fails discovery.
1766
+ const errors = parsed.errors.filter((error) => error.code !== 18002 && error.code !== 18003);
1767
+ if (errors.length > 0) {
1768
+ process.stderr.write(errors.map((error) => "TS" + error.code + ": " + (error.text ?? error.messageText ?? "")).join("\\n"));
1769
+ process.exitCode = 1;
1770
+ } else
1771
+ process.stdout.write(JSON.stringify({
1772
+ files: parsed.fileNames,
1773
+ references: (parsed.projectReferences ?? []).map((reference) => reference.path),
1774
+ allowJs: parsed.options.allowJs === true,
1775
+ checkJs: parsed.options.checkJs === true,
1776
+ }));
1777
+ } finally {
1778
+ await api.close();
1779
+ }
1780
+ `;
1781
+ const resolveProjectApi = (root) => {
1782
+ for (const base of [require("url").pathToFileURL(__filename).href, node_path.default.join(root, "package.json")]) try {
1783
+ return (0, node_url.pathToFileURL)((0, node_module.createRequire)(base).resolve("tsrs/unstable/async")).href;
1784
+ } catch (error) {
1785
+ const code = record$4(error) ? error["code"] : void 0;
1786
+ if (code !== "MODULE_NOT_FOUND" && code !== "ERR_MODULE_NOT_FOUND") throw error;
1787
+ }
1788
+ throw new Error("configuration discovery needs tsrs: reinstall @stll/oxlint-config, which depends on it");
1789
+ };
1790
+ const readProjectConfig = (root, config) => {
1791
+ const api = resolveProjectApi(root);
1792
+ const result = (0, node_child_process.spawnSync)("node", [
1793
+ "--input-type=module",
1794
+ "-e",
1795
+ projectConfigScript,
1796
+ api,
1797
+ root,
1798
+ node_path.default.resolve(root, config)
1799
+ ], {
1800
+ cwd: root,
1801
+ encoding: "utf8",
1802
+ timeout: 3e4,
1803
+ maxBuffer: 33554432
1804
+ });
1805
+ if (result.error !== void 0) throw result.error;
1806
+ if (result.status !== 0) throw new Error(`configuration discovery failed: ${(result.stderr + result.stdout).trim()}`);
1807
+ const parsed = JSON.parse(result.stdout);
1808
+ if (!record$4(parsed) || !stringList(parsed["files"]) || !stringList(parsed["references"]) || typeof parsed["allowJs"] !== "boolean" || typeof parsed["checkJs"] !== "boolean") throw new Error("compiler configuration must list files and references");
1809
+ return {
1810
+ files: parsed["files"],
1811
+ references: parsed["references"],
1812
+ allowJs: parsed["allowJs"],
1813
+ checkJs: parsed["checkJs"]
1814
+ };
1815
+ };
1742
1816
  const relativePath = (root, file) => node_path.default.relative(root, node_path.default.resolve(root, file)).split(node_path.default.sep).join("/");
1743
1817
  const lifecycleFile = (root, file, tracked) => {
1744
1818
  try {
@@ -1837,7 +1911,6 @@ const checkNodeProjects = ({ root, manifest, trackedFiles }) => {
1837
1911
  try {
1838
1912
  const require$1 = (0, node_module.createRequire)(node_path.default.join(root, "package.json"));
1839
1913
  const oxlint = node_path.default.join(node_path.default.dirname(require$1.resolve("oxlint/package.json")), "bin/oxlint");
1840
- const compiler = node_path.default.join(node_path.default.dirname(require$1.resolve("typescript/package.json")), "bin/tsc");
1841
1914
  const commands = inventory.commands.map((command) => ({
1842
1915
  cwd: root,
1843
1916
  ...command
@@ -1899,32 +1972,16 @@ const checkNodeProjects = ({ root, manifest, trackedFiles }) => {
1899
1972
  references: []
1900
1973
  };
1901
1974
  try {
1902
- const project = JSON.parse(readCommand({
1903
- root,
1904
- executable: compiler,
1905
- args: [
1906
- "--showConfig",
1907
- "--project",
1908
- config
1909
- ],
1910
- runtime: "node"
1911
- }));
1912
- if (!record$4(project)) throw new Error("compiler configuration must be an object");
1913
- const references = project["references"] ?? [];
1914
- if (!Array.isArray(references)) throw new Error("compiler configuration references must be an array");
1915
- for (const reference of references) {
1916
- if (!record$4(reference) || typeof reference["path"] !== "string") throw new Error("compiler project reference requires a path");
1917
- const target = relativePath(root, node_path.default.resolve(root, node_path.default.dirname(config), reference["path"]));
1975
+ const project = readProjectConfig(root, config);
1976
+ for (const reference of project.references) {
1977
+ const target = relativePath(root, reference);
1918
1978
  const referencedConfig = target.endsWith(".json") ? target : node_path.default.posix.join(target, "tsconfig.json");
1919
1979
  if (!trackedFiles.has(referencedConfig)) throw new Error(`project reference is not tracked: ${referencedConfig}`);
1920
1980
  result.references.push(referencedConfig);
1921
1981
  }
1922
- const sources = project["files"] ?? [];
1923
- if (!stringList(sources)) throw new Error("compiler configuration did not return a files array");
1924
- const options = project["compilerOptions"];
1925
- for (const file of sources) {
1926
- if (scriptSourceKind(file) === "javascript" && (!record$4(options) || options["allowJs"] !== true && options["checkJs"] !== true)) continue;
1927
- result.files.add(relativePath(root, node_path.default.resolve(root, node_path.default.dirname(config), file)));
1982
+ for (const file of project.files) {
1983
+ if (scriptSourceKind(file) === "javascript" && !project.allowJs && !project.checkJs) continue;
1984
+ result.files.add(relativePath(root, file));
1928
1985
  }
1929
1986
  } catch (error) {
1930
1987
  result.files.clear();
@@ -1972,8 +2029,11 @@ const checkNodeProjects = ({ root, manifest, trackedFiles }) => {
1972
2029
  //#endregion
1973
2030
  //#region src/toolchain-rust.ts
1974
2031
  const record$3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1975
- /** Exceptions authorize one immutable release, never a channel or an entire rule. */
1976
- const parseRustExceptions = (input, today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)) => {
2032
+ /**
2033
+ * Exceptions authorize one immutable release, never a channel or an entire
2034
+ * rule. Expiry is scoped separately by checkRustExceptionExpiry.
2035
+ */
2036
+ const parseRustExceptions = (input) => {
1977
2037
  if (input === void 0) return [];
1978
2038
  if (!Array.isArray(input)) throw new Error("rustExceptions must be an array");
1979
2039
  const pins = /* @__PURE__ */ new Set();
@@ -1983,7 +2043,7 @@ const parseRustExceptions = (input, today = (/* @__PURE__ */ new Date()).toISOSt
1983
2043
  "reason",
1984
2044
  "tracking",
1985
2045
  "expires"
1986
- ].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");
2046
+ ].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"] || pins.has(entry["pin"])) throw new Error("each Rust exception requires a unique exact stable pin, reason, GitHub issue and valid expiry date");
1987
2047
  pins.add(entry["pin"]);
1988
2048
  return {
1989
2049
  pin: entry["pin"],
@@ -1993,6 +2053,30 @@ const parseRustExceptions = (input, today = (/* @__PURE__ */ new Date()).toISOSt
1993
2053
  };
1994
2054
  });
1995
2055
  };
2056
+ /** Whether the base stll-toolchain.json declares this exception with the same expiry. */
2057
+ const declaresException = (baseText, { pin, expires }) => {
2058
+ const base = JSON.parse(baseText);
2059
+ const exceptions = record$3(base) ? base["rustExceptions"] : void 0;
2060
+ return Array.isArray(exceptions) && exceptions.some((entry) => record$3(entry) && entry["pin"] === pin && entry["expires"] === expires);
2061
+ };
2062
+ /** An exception expires on its date; the scope decides whether that fails this run. */
2063
+ const checkRustExceptionExpiry = ({ exceptions, now = /* @__PURE__ */ new Date(), scope }) => {
2064
+ const today = now.toISOString().slice(0, 10);
2065
+ const errors = [];
2066
+ const warnings = [];
2067
+ for (const exception of exceptions) {
2068
+ if (exception.expires > today) continue;
2069
+ const overdue = `stll-toolchain.json Rust exception ${exception.pin} expired on ${exception.expires}`;
2070
+ const verdict = require_quarantine_policy.overdueVerdict(scope, "stll-toolchain.json", (base) => declaresException(base, exception));
2071
+ if (!verdict.fail) warnings.push(`${overdue}; remove it or move to the shared Rust pin (default-branch and scheduled runs fail, as do changes that add or edit it)`);
2072
+ else if (verdict.unavailable === void 0) errors.push(`${overdue}; remove it or move to the shared Rust pin`);
2073
+ else errors.push(`${overdue}; cannot tell whether this change adds it: ${verdict.unavailable}`);
2074
+ }
2075
+ return {
2076
+ errors,
2077
+ warnings
2078
+ };
2079
+ };
1996
2080
  /** Only executable fields are inspected: unrelated action inputs are not Rust selectors. */
1997
2081
  const checkRustReferences = (options) => {
1998
2082
  const { file, rust, exceptions } = options;
@@ -3895,7 +3979,7 @@ const record = (value) => typeof value === "object" && value !== null && !Array.
3895
3979
  const array = (value) => Array.isArray(value);
3896
3980
  /** Repository-owned declarations are explicit; malformed decisions fail closed. */
3897
3981
  const parseToolchainConfiguration = (input) => {
3898
- if (!record(input) || Object.keys(input).some((key) => key !== "optOuts" && key !== "cloud" && key !== "engineFloors" && key !== "dynamicSelectors" && key !== "consumerChecks" && key !== "rustExceptions" && key !== "dependabotIgnores") || input["optOuts"] !== void 0 && !array(input["optOuts"])) throw new Error("stll-toolchain.json accepts only optOuts, cloud, engineFloors, dynamicSelectors, consumerChecks, rustExceptions and dependabotIgnores declarations");
3982
+ if (!record(input) || Object.keys(input).some((key) => key !== "optOuts" && key !== "cloud" && key !== "engineFloors" && key !== "dynamicSelectors" && key !== "consumerChecks" && key !== "rustExceptions" && key !== "dependabotIgnores" && key !== "typescriptAllowlist") || input["optOuts"] !== void 0 && !array(input["optOuts"])) throw new Error("stll-toolchain.json accepts only optOuts, cloud, engineFloors, dynamicSelectors, consumerChecks, rustExceptions, dependabotIgnores and typescriptAllowlist declarations");
3899
3983
  const disabled = /* @__PURE__ */ new Set();
3900
3984
  const optOuts = input["optOuts"] ?? [];
3901
3985
  if (!array(optOuts)) throw new Error("optOuts must be an array");
@@ -3916,7 +4000,8 @@ const parseToolchainConfiguration = (input) => {
3916
4000
  engineFloors: parseEngineFloors(input["engineFloors"]),
3917
4001
  dynamicSelectors: parseDynamicSelectors(input["dynamicSelectors"]),
3918
4002
  consumerChecks: parseConsumerChecks(input["consumerChecks"]),
3919
- dependabotIgnores: require_toolchain_dependabot.parseDependabotIgnores(input["dependabotIgnores"])
4003
+ dependabotIgnores: require_toolchain_dependabot.parseDependabotIgnores(input["dependabotIgnores"]),
4004
+ typescriptAllowlist: parseTypescriptAllowlist(input["typescriptAllowlist"])
3920
4005
  };
3921
4006
  };
3922
4007
  /** Dependency graphs stay opaque unless a caller explicitly needs registry identities. */
@@ -3974,8 +4059,12 @@ const readToolchainInputs = (root, { bunLock = "presence" } = {}) => {
3974
4059
  diagnostics
3975
4060
  };
3976
4061
  };
3977
- const checkToolchain = ({ root, policy, onDelegated, onWarning, now }) => {
4062
+ const checkToolchain = ({ root, policy: sharedPolicy, onDelegated, onWarning, now, expiryScope = { kind: "enforce" } }) => {
3978
4063
  const { files, trackedFiles, diagnostics } = readToolchainInputs(root, { bunLock: "content" });
4064
+ const policy = {
4065
+ ...sharedPolicy,
4066
+ bun: repositoryBun(sharedPolicy.bun, files["package.json"])
4067
+ };
3979
4068
  let repository;
3980
4069
  try {
3981
4070
  const origin = (0, node_child_process.execFileSync)("git", [
@@ -3999,6 +4088,23 @@ const checkToolchain = ({ root, policy, onDelegated, onWarning, now }) => {
3999
4088
  if (files["stll-toolchain.json"] !== void 0) try {
4000
4089
  configuration = parseToolchainConfiguration(JSON.parse(files["stll-toolchain.json"]));
4001
4090
  dynamicSelectors = configuration.dynamicSelectors;
4091
+ const expiry = checkRustExceptionExpiry({
4092
+ exceptions: configuration.rustExceptions,
4093
+ now,
4094
+ scope: expiryScope
4095
+ });
4096
+ for (const message of expiry.errors) diagnostics.push({
4097
+ rule: "configuration",
4098
+ path: "stll-toolchain.json",
4099
+ line: 1,
4100
+ message
4101
+ });
4102
+ for (const message of expiry.warnings) onWarning?.({
4103
+ rule: "configuration",
4104
+ path: "stll-toolchain.json",
4105
+ line: 1,
4106
+ message
4107
+ });
4002
4108
  for (const entry of configuration.engineFloors) engineFloors.push(resolveEngineFloor(entry, files));
4003
4109
  } catch (error) {
4004
4110
  diagnostics.push({
@@ -4063,7 +4169,8 @@ const checkToolchain = ({ root, policy, onDelegated, onWarning, now }) => {
4063
4169
  }));
4064
4170
  diagnostics.push(...checkPackageFiles({
4065
4171
  files,
4066
- policy
4172
+ policy,
4173
+ typescriptAllowlist: configuration.typescriptAllowlist
4067
4174
  }));
4068
4175
  const usesBun = Object.entries(files).some(([file, text]) => {
4069
4176
  if (node_path.default.posix.basename(file) === "bun.lock") return true;
@@ -4085,7 +4192,8 @@ const checkToolchain = ({ root, policy, onDelegated, onWarning, now }) => {
4085
4192
  const result = require_quarantine_policy.checkRepositoryQuarantine({
4086
4193
  files,
4087
4194
  now,
4088
- npmrcPresent: trackedFiles.has(".npmrc")
4195
+ npmrcPresent: trackedFiles.has(".npmrc"),
4196
+ expiry: expiryScope
4089
4197
  });
4090
4198
  for (const message of result.errors) diagnostics.push({
4091
4199
  rule: "quarantine-policy",
@@ -4189,5 +4297,11 @@ Object.defineProperty(exports, "readToolchainInputs", {
4189
4297
  return readToolchainInputs;
4190
4298
  }
4191
4299
  });
4300
+ Object.defineProperty(exports, "repositoryBun", {
4301
+ enumerable: true,
4302
+ get: function() {
4303
+ return repositoryBun;
4304
+ }
4305
+ });
4192
4306
 
4193
4307
  //# sourceMappingURL=toolchain-guard.cjs.map