@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
@@ -1,4 +1,4 @@
1
- import { t as checkRepositoryQuarantine } from "./quarantine-policy2.mjs";
1
+ import { l as overdueVerdict, t as checkRepositoryQuarantine } from "./quarantine-policy2.mjs";
2
2
  import { C as cloudSetupPath, D as nodeSupportRangeMatches, E as nodeSelectorMatches, O as parseCloudSetup, _ as cloudAttributionFix, b as hasCloudAttribution, f as toolchainInputKind, g as workspaceContains, h as isKubernetesDefinitionPath, i as isDockerDefinitionPath, l as isShellScript, m as isComposeDefinitionPath, n as githubAutomationFileKind, p as containerDocumentImages, s as isMiseConfigPath, v as cloudClaudeSettingsPath, w as generateCloudSetup, x as hasCloudSessionStartHook, y as cloudSessionStartFix } from "./toolchain-inputs.mjs";
3
3
  import { t as readGitTrackedOutput } from "./git-tracked-output.mjs";
4
4
  import { a as assertConsumerFixtureSelection, g as parseConsumerFixtures, h as discoverConsumerPackages, m as discoverConsumerManifests, r as assertConsumerFixtureKind, t as assertConsumerNodeSupport, u as consumerPackageClosure } from "./consumer-node-support.mjs";
@@ -6,15 +6,32 @@ import { d as rustImageVersion, i as dependabotRules, n as declaredIgnoreConflic
6
6
  import { n as checkFormatter, r as formatterRule } from "./toolchain-schema.mjs";
7
7
  import { createRequire } from "node:module";
8
8
  import path, { extname } from "node:path";
9
+ import { execFileSync, spawnSync } from "node:child_process";
9
10
  import { closeSync, lstatSync, openSync, readFileSync, readSync, realpathSync } from "node:fs";
10
11
  import { parse } from "smol-toml";
11
- import { execFileSync, spawnSync } from "node:child_process";
12
- import { minVersion, satisfies, valid, validRange } from "semver";
12
+ import { gte, minVersion, satisfies, valid, validRange } from "semver";
13
13
  import { LineCounter, isAlias, isMap, isNode, isScalar, isSeq, parseAllDocuments, parseDocument } from "yaml";
14
14
  import picomatch from "picomatch";
15
+ import { pathToFileURL } from "node:url";
16
+ //#region src/toolchain-bun.ts
17
+ const STABLE_RELEASE = /^\d+\.\d+\.\d+$/u;
18
+ /**
19
+ * The shared policy names the minimum Bun. A repository pins one exact
20
+ * release at or above it in its root packageManager, and every other Bun
21
+ * reference in the repository must equal that release.
22
+ */
23
+ const repositoryBun = (floor, manifest) => {
24
+ let packageManager;
25
+ try {
26
+ const parsed = JSON.parse(manifest ?? "");
27
+ if (typeof parsed === "object" && parsed !== null) packageManager = Reflect.get(parsed, "packageManager");
28
+ } catch {}
29
+ const version = typeof packageManager === "string" ? /^bun@(.+)$/.exec(packageManager)?.[1] : void 0;
30
+ return version !== void 0 && STABLE_RELEASE.test(version) && valid(version) === version && gte(version, floor) ? version : floor;
31
+ };
15
32
  //#endregion
16
33
  //#region src/toolchain-consumer-checks.ts
17
- const consumerRunnerVersion = "1.8.0";
34
+ const consumerRunnerVersion = "2.0.0";
18
35
  const consumerCheckWorkflow = "stella/.github/.github/workflows/package-consumer-compat.yml";
19
36
  const record$10 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
20
37
  const repositoryDirectory = (value) => typeof value === "string" && value !== "" && !value.includes("\\") && !value.includes("${{") && !value.split("").some((character) => {
@@ -349,7 +366,71 @@ const packageRules = [
349
366
  "typescript-layout",
350
367
  "node-engine"
351
368
  ];
369
+ /** Compiler packages `bun check` replaces; an install needs a repository allowlist entry. */
370
+ const typescriptPackages = /* @__PURE__ */ new Set(["typescript", "@typescript/native"]);
371
+ const typecheckCommand = "bun check";
372
+ 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";
373
+ const installSections = [
374
+ "dependencies",
375
+ "devDependencies",
376
+ "optionalDependencies"
377
+ ];
378
+ const allowlistFile = "stll-toolchain.json";
352
379
  const record$7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
380
+ /** `npm:typescript@7.0.2` installs a compiler under another name; returns its version. */
381
+ const typescriptAliasVersion = (spec) => {
382
+ if (typeof spec !== "string") return void 0;
383
+ const match = /^npm:(?:typescript|@typescript\/native)(?:@(.*))?$/.exec(spec);
384
+ return match === null ? void 0 : match[1] ?? "";
385
+ };
386
+ const exactTypescriptSpec = (spec) => {
387
+ if (typeof spec !== "string") return false;
388
+ const version = typescriptAliasVersion(spec) ?? spec;
389
+ return valid(version) === version;
390
+ };
391
+ const isTypescriptInstall = (name, value) => typescriptPackages.has(name) || typescriptAliasVersion(value) !== void 0;
392
+ const allowlistKeys = [
393
+ "workspace",
394
+ "name",
395
+ "spec",
396
+ "dependency",
397
+ "reason"
398
+ ];
399
+ const allowlistFormat = "{\"workspace\": \"<package directory or .>\", \"name\": \"typescript\", \"spec\": \"<exact version>\", \"dependency\": \"<package that needs it>\", \"reason\": \"<why>\"}";
400
+ const workspaceManifest = (workspace) => workspace === "." ? "package.json" : `${workspace}/package.json`;
401
+ /** Each repository declares its own TypeScript installs; there is no shared list. */
402
+ const parseTypescriptAllowlist = (input) => {
403
+ if (input === void 0) return [];
404
+ if (!Array.isArray(input)) throw new Error(`typescriptAllowlist must be an array of entries like ${allowlistFormat}`);
405
+ const seen = /* @__PURE__ */ new Set();
406
+ return input.map((entry) => {
407
+ const shown = JSON.stringify(entry);
408
+ const text = (key) => {
409
+ const value = record$7(entry) ? entry[key] : void 0;
410
+ return typeof value === "string" && value.trim() !== "" ? value : void 0;
411
+ };
412
+ const workspace = text("workspace");
413
+ const name = text("name");
414
+ const spec = text("spec");
415
+ const dependency = text("dependency");
416
+ const reason = text("reason");
417
+ 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}`);
418
+ if (workspace !== "." && (path.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"`);
419
+ 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`);
420
+ 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>`);
421
+ 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`);
422
+ const key = `${workspace}\0${name}`;
423
+ if (seen.has(key)) throw new Error(`typescriptAllowlist declares ${name} for ${workspace} more than once; keep one entry`);
424
+ seen.add(key);
425
+ return {
426
+ workspace,
427
+ name,
428
+ spec,
429
+ dependency,
430
+ reason
431
+ };
432
+ });
433
+ };
353
434
  const shellSubstitutions = (command) => {
354
435
  const frames = [{
355
436
  kind: "root",
@@ -875,7 +956,7 @@ const invokesCompiler = (words) => {
875
956
  return tokens.slice(start).some((word, index) => compiler(word) || word === "bun" && tokens.at(skipOptions(start + index + 1, "bun")) === "check");
876
957
  };
877
958
  /** Validate manifests and catalog resolutions from a tracked-file snapshot. */
878
- const checkPackageFiles = ({ files, policy }) => {
959
+ const checkPackageFiles = ({ files, policy, typescriptAllowlist }) => {
879
960
  const sources = new Map(Object.entries(files));
880
961
  const diagnostics = [];
881
962
  const pins = new Map(Object.entries({
@@ -991,16 +1072,29 @@ const checkPackageFiles = ({ files, policy }) => {
991
1072
  directory = path.posix.dirname(directory);
992
1073
  }
993
1074
  };
994
- const resolve = (file, name, value) => {
1075
+ const resolveThrough = (file, name, value) => {
995
1076
  const visited = /* @__PURE__ */ new Set();
1077
+ const catalogs = [];
996
1078
  while (typeof value === "string" && value.startsWith("catalog:")) {
997
- if (visited.has(value)) return void 0;
1079
+ if (visited.has(value)) return {
1080
+ value: void 0,
1081
+ catalogs
1082
+ };
998
1083
  visited.add(value);
999
1084
  const catalog = catalogFor(file, value.slice(8));
1000
- value = record$7(catalog) ? catalog[name] : void 0;
1085
+ if (!record$7(catalog)) return {
1086
+ value: void 0,
1087
+ catalogs
1088
+ };
1089
+ catalogs.push(catalog);
1090
+ value = catalog[name];
1001
1091
  }
1002
- return value;
1092
+ return {
1093
+ value,
1094
+ catalogs
1095
+ };
1003
1096
  };
1097
+ const resolve = (file, name, value) => resolveThrough(file, name, value).value;
1004
1098
  const workspaceOwnerFor = (file) => {
1005
1099
  const pnpm = pnpmWorkspaceFor(file);
1006
1100
  if (pnpm !== void 0) return {
@@ -1039,88 +1133,49 @@ const checkPackageFiles = ({ files, policy }) => {
1039
1133
  }));
1040
1134
  return candidates.length === 1 && candidates.every(([, json]) => json["version"] === pin) ? "matched" : "mismatch";
1041
1135
  };
1042
- const tsSpecifiers = /* @__PURE__ */ new Map();
1043
- for (const layout of policy.typescriptInstallLayouts) {
1044
- const values = tsSpecifiers.get(layout.compilerPackage) ?? /* @__PURE__ */ new Set();
1045
- values.add(layout.compilerSpecifier);
1046
- tsSpecifiers.set(layout.compilerPackage, values);
1047
- if (layout.type !== "split-compatibility") continue;
1048
- const compatibility = tsSpecifiers.get(layout.compatibilityPackage) ?? /* @__PURE__ */ new Set();
1049
- compatibility.add(layout.compatibilitySpecifier);
1050
- tsSpecifiers.set(layout.compatibilityPackage, compatibility);
1051
- }
1052
- tsSpecifiers.set(policy.typescript6Compatibility.packageAlias, /* @__PURE__ */ new Set([`npm:typescript@${policy.typescript6Compatibility.version}`]));
1053
- const layoutFor = (specifiers) => policy.typescriptInstallLayouts.find((layout) => {
1054
- const compiler = specifiers.get(layout.compilerPackage);
1055
- if (compiler?.size !== 1 || !compiler.has(layout.compilerSpecifier)) return false;
1056
- if (layout.type === "split-compatibility") {
1057
- const compatibility = specifiers.get(layout.compatibilityPackage);
1058
- return compatibility?.size === 1 && compatibility.has(layout.compatibilitySpecifier);
1059
- }
1060
- return policy.typescriptInstallLayouts.every((other) => other.compilerPackage === layout.compilerPackage || !specifiers.has(other.compilerPackage));
1061
- });
1062
- const peerRange = (value) => typeof value === "string" && valid(value) === null && validRange(value) !== null;
1063
- const toolchainEntries = (file, json) => {
1064
- const entries = [];
1065
- for (const section of [
1066
- "dependencies",
1067
- "devDependencies",
1068
- "optionalDependencies",
1069
- "peerDependencies"
1070
- ]) {
1071
- const dependencies = json[section];
1072
- if (!record$7(dependencies)) continue;
1073
- for (const [name, value] of Object.entries(dependencies)) {
1074
- if (!tsSpecifiers.has(name)) continue;
1075
- const resolved = resolve(file, name, value);
1076
- if (section === "peerDependencies" && peerRange(resolved)) continue;
1077
- entries.push({
1078
- section,
1079
- name,
1080
- value: resolved
1081
- });
1136
+ const allowed = new Map(typescriptAllowlist.map((entry) => [`${workspaceManifest(entry.workspace)}\0${entry.name}`, entry]));
1137
+ const allowedCatalogEntries = /* @__PURE__ */ new Map();
1138
+ for (const entry of typescriptAllowlist) {
1139
+ const file = workspaceManifest(entry.workspace);
1140
+ const json = manifests.get(file);
1141
+ if (json === void 0) continue;
1142
+ for (const section of installSections) {
1143
+ const entries = json[section];
1144
+ if (!record$7(entries) || !Object.hasOwn(entries, entry.name)) continue;
1145
+ const { value, catalogs } = resolveThrough(file, entry.name, entries[entry.name]);
1146
+ if (value !== entry.spec) continue;
1147
+ for (const catalog of catalogs) {
1148
+ const names = allowedCatalogEntries.get(catalog) ?? /* @__PURE__ */ new Set();
1149
+ names.add(entry.name);
1150
+ allowedCatalogEntries.set(catalog, names);
1082
1151
  }
1083
1152
  }
1084
- return entries;
1085
- };
1086
- const specifierMap = (entries) => {
1087
- const specifiers = /* @__PURE__ */ new Map();
1088
- for (const { name, value } of entries) {
1089
- const values = specifiers.get(name) ?? /* @__PURE__ */ new Set();
1090
- values.add(value);
1091
- specifiers.set(name, values);
1092
- }
1093
- return specifiers;
1153
+ }
1154
+ const shownSpec = (raw, value) => {
1155
+ if (typeof value !== "string") return `${String(raw)} (unresolved)`;
1156
+ return raw === value ? value : `${value} (from ${String(raw)})`;
1094
1157
  };
1095
- const workspaceLayouts = /* @__PURE__ */ new Map();
1096
- const workspaceLayoutFor = (file) => {
1097
- const owner = workspaceOwnerFor(file);
1098
- if (owner === void 0 || !workspaceContains({
1099
- ...owner,
1100
- file,
1101
- rootMembership: "implicit"
1102
- })) return void 0;
1103
- const key = `${owner.source}:${owner.directory}`;
1104
- const cached = workspaceLayouts.get(key);
1105
- if (cached !== void 0) return cached;
1106
- const entries = [];
1107
- for (const [candidate, member] of manifests) {
1108
- if (path.posix.basename(candidate) !== "package.json") continue;
1109
- const memberOwner = workspaceOwnerFor(candidate);
1110
- if (memberOwner?.directory !== owner.directory || memberOwner.source !== owner.source || !workspaceContains({
1111
- ...owner,
1112
- file: candidate,
1113
- rootMembership: "implicit"
1114
- })) continue;
1115
- entries.push(...toolchainEntries(candidate, member).filter(({ section }) => section === "devDependencies"));
1158
+ const typescriptMessage = (name, value) => `${name} ${String(value)} is not needed: ${typescriptGuidance}`;
1159
+ const typescriptEntryMessage = ({ file, entries, name, raw, value, scope }) => {
1160
+ const shown = shownSpec(raw, value);
1161
+ 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}`;
1162
+ 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}`;
1163
+ if (scope === "peer") return `${name} peer ${shown} is not a version range: ${typescriptGuidance}`;
1164
+ const entry = path.posix.basename(file) === "package.json" ? allowed.get(`${file}\0${name}`) : void 0;
1165
+ if (entry === void 0) {
1166
+ const workspace = path.posix.dirname(file);
1167
+ const suggestion = JSON.stringify({
1168
+ workspace,
1169
+ name,
1170
+ spec: exactTypescriptSpec(value) ? value : "<exact version>",
1171
+ dependency: "<package that needs it>",
1172
+ reason: "<why it needs this TypeScript>"
1173
+ });
1174
+ return `${name} ${shown} in ${file} is not allowed: ${typescriptGuidance}; if a dependency of this workspace needs it, add ${suggestion} to typescriptAllowlist in ${allowlistFile}`;
1116
1175
  }
1117
- const result = {
1118
- layout: layoutFor(specifierMap(entries)),
1119
- directory: owner.directory
1120
- };
1121
- workspaceLayouts.set(key, result);
1122
- return result;
1176
+ 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`;
1123
1177
  };
1178
+ const peerRange = (value) => typeof value === "string" && validRange(value) !== null;
1124
1179
  for (const [file, json] of manifests) {
1125
1180
  const manager = json["packageManager"];
1126
1181
  if (manager !== void 0 && manager !== `bun@${policy.bun}`) add({
@@ -1128,20 +1183,27 @@ const checkPackageFiles = ({ files, policy }) => {
1128
1183
  rule: "bun-pins",
1129
1184
  key: "packageManager",
1130
1185
  value: manager,
1131
- message: `packageManager Bun version must be ${policy.bun}, found ${JSON.stringify(manager)}`
1186
+ 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)}`
1132
1187
  });
1133
- const checkEntries = (entries, location) => {
1188
+ const checkEntries = (entries, scope, location) => {
1134
1189
  if (!record$7(entries)) return;
1135
1190
  for (const [name, raw] of Object.entries(entries)) {
1136
1191
  const value = resolve(file, name, raw);
1137
- const tsAllowed = tsSpecifiers.get(name);
1138
- if (tsAllowed !== void 0) {
1139
- if (typeof value !== "string" || !tsAllowed.has(value)) add({
1192
+ if (isTypescriptInstall(name, value)) {
1193
+ const message = typescriptEntryMessage({
1194
+ file,
1195
+ entries,
1196
+ name,
1197
+ raw,
1198
+ value,
1199
+ scope
1200
+ });
1201
+ if (message !== void 0) add({
1140
1202
  file,
1141
1203
  rule: "typescript-layout",
1142
1204
  key: location?.key ?? name,
1143
1205
  value: location === void 0 ? raw : location.value,
1144
- message: `${name} must use a declared TypeScript specifier, found ${String(value)}`
1206
+ message
1145
1207
  });
1146
1208
  continue;
1147
1209
  }
@@ -1163,20 +1225,17 @@ const checkPackageFiles = ({ files, policy }) => {
1163
1225
  });
1164
1226
  }
1165
1227
  };
1166
- for (const key of [
1167
- "dependencies",
1168
- "devDependencies",
1169
- "optionalDependencies"
1170
- ]) checkEntries(json[key]);
1171
- if (record$7(json["peerDependencies"])) {
1172
- for (const [name, value] of Object.entries(json["peerDependencies"])) if (tsSpecifiers.has(name) && !peerRange(resolve(file, name, value))) checkEntries({ [name]: value });
1228
+ for (const key of installSections) checkEntries(json[key], "install");
1229
+ if (record$7(json["peerDependencies"])) for (const [name, value] of Object.entries(json["peerDependencies"])) {
1230
+ const resolved = resolve(file, name, value);
1231
+ if (isTypescriptInstall(name, resolved) && !peerRange(resolved)) checkEntries({ [name]: value }, "peer");
1173
1232
  }
1174
1233
  for (const source of catalogSources(json)) {
1175
- checkEntries(source["catalog"]);
1234
+ checkEntries(source["catalog"], "catalog");
1176
1235
  if (!record$7(source["catalogs"])) continue;
1177
- for (const catalog of Object.values(source["catalogs"])) checkEntries(catalog);
1236
+ for (const catalog of Object.values(source["catalogs"])) checkEntries(catalog, "catalog");
1178
1237
  }
1179
- const ownedNames = /* @__PURE__ */ new Set([...pins.keys(), ...tsSpecifiers.keys()]);
1238
+ const ownedNames = /* @__PURE__ */ new Set([...pins.keys(), ...typescriptPackages]);
1180
1239
  const resolutionDependencies = {};
1181
1240
  const resolutionRoot = manifests.get(path.posix.join(path.posix.dirname(file), "package.json"));
1182
1241
  if (resolutionRoot !== void 0) for (const section of [
@@ -1188,32 +1247,6 @@ const checkPackageFiles = ({ files, policy }) => {
1188
1247
  if (!record$7(entries)) continue;
1189
1248
  for (const [name, value] of Object.entries(entries)) resolutionDependencies[name] = resolve(file, name, value);
1190
1249
  }
1191
- const affectedSpecifiers = /* @__PURE__ */ new Map();
1192
- const owner = workspaceOwnerFor(file);
1193
- for (const [candidate, member] of manifests) {
1194
- if (path.posix.basename(candidate) !== "package.json") continue;
1195
- const memberOwner = workspaceOwnerFor(candidate);
1196
- if (!(candidate === path.posix.join(path.posix.dirname(file), "package.json")) && !(owner !== void 0 && owner.directory === path.posix.dirname(file) && memberOwner?.directory === owner.directory && memberOwner.source === owner.source && workspaceContains({
1197
- directory: owner.directory,
1198
- file: candidate,
1199
- patterns: owner.patterns,
1200
- rootMembership: "implicit"
1201
- }))) continue;
1202
- for (const section of [
1203
- "dependencies",
1204
- "devDependencies",
1205
- "optionalDependencies"
1206
- ]) {
1207
- const entries = member[section];
1208
- if (!record$7(entries)) continue;
1209
- for (const [name, value] of Object.entries(entries)) {
1210
- if (!tsSpecifiers.has(name)) continue;
1211
- const values = affectedSpecifiers.get(name) ?? /* @__PURE__ */ new Set();
1212
- values.add(resolve(candidate, name, value));
1213
- affectedSpecifiers.set(name, values);
1214
- }
1215
- }
1216
- }
1217
1250
  const checkResolutionMap = (entries, parent) => {
1218
1251
  if (!record$7(entries)) return;
1219
1252
  for (const [selector, replacement] of Object.entries(entries)) {
@@ -1221,11 +1254,7 @@ const checkPackageFiles = ({ files, policy }) => {
1221
1254
  if (target === void 0) continue;
1222
1255
  let pattern = target.split(/>\s*(?=[@A-Za-z_*]|$)/).at(-1)?.trim() ?? "";
1223
1256
  const version = pattern.lastIndexOf("@");
1224
- let qualifier;
1225
- if (version > pattern.lastIndexOf("/")) {
1226
- qualifier = pattern.slice(version + 1).replace(/^npm:/, "");
1227
- pattern = pattern.slice(0, version);
1228
- }
1257
+ if (version > pattern.lastIndexOf("/")) pattern = pattern.slice(0, version);
1229
1258
  const parts = pattern.split("/");
1230
1259
  pattern = parts.at(-2)?.startsWith("@") ? parts.slice(-2).join("/") : parts.at(-1) ?? "";
1231
1260
  if (pattern === "") {
@@ -1242,102 +1271,60 @@ const checkPackageFiles = ({ files, policy }) => {
1242
1271
  checkResolutionMap(replacement, target);
1243
1272
  continue;
1244
1273
  }
1245
- for (const name of ownedNames) {
1246
- if (!picomatch(pattern)(name)) continue;
1247
- let value = replacement;
1248
- if (typeof replacement === "string" && replacement.startsWith("$")) {
1249
- const reference = replacement.slice(1);
1250
- value = resolutionDependencies[reference];
1251
- }
1252
- const resolved = resolve(file, name, value);
1253
- checkEntries({ [name]: value }, {
1254
- key: selector,
1255
- value: replacement
1256
- });
1257
- if (tsSpecifiers.has(name) && [...affectedSpecifiers.get(name) ?? []].some((specifier) => {
1258
- if (resolved === specifier) return false;
1259
- if (qualifier === void 0 || validRange(qualifier) === null) return true;
1260
- if (typeof specifier !== "string") return false;
1261
- const versionSpecifier = specifier.startsWith("npm:") ? specifier.slice(specifier.lastIndexOf("@") + 1) : specifier;
1262
- const release = valid(versionSpecifier);
1263
- return release !== null && satisfies(release, qualifier);
1264
- }) && typeof resolved === "string" && tsSpecifiers.get(name)?.has(resolved)) add({
1265
- file,
1266
- rule: "typescript-layout",
1267
- key: selector,
1268
- value: replacement,
1269
- message: `${name} resolution must preserve its declared TypeScript install specifier`
1270
- });
1274
+ let value = replacement;
1275
+ if (typeof replacement === "string" && replacement.startsWith("$")) {
1276
+ const reference = replacement.slice(1);
1277
+ value = resolutionDependencies[reference];
1271
1278
  }
1279
+ const location = {
1280
+ key: selector,
1281
+ value: replacement
1282
+ };
1283
+ const targets = [...ownedNames].filter((name) => picomatch(pattern)(name));
1284
+ for (const name of targets) checkEntries({ [name]: value }, "resolution", location);
1285
+ if (typescriptAliasVersion(value) !== void 0 && !targets.some((name) => isTypescriptInstall(name, value))) checkEntries({ [pattern]: value }, "resolution", location);
1272
1286
  }
1273
1287
  };
1274
1288
  checkResolutionMap(json["overrides"]);
1275
1289
  checkResolutionMap(json["resolutions"]);
1276
1290
  if (record$7(json["pnpm"])) checkResolutionMap(json["pnpm"]["overrides"]);
1277
- const localEntries = toolchainEntries(file, json);
1278
- const usesCompiler = policy.typescriptInstallLayouts.some((layout) => localEntries.some(({ name }) => name === layout.compilerPackage));
1279
- let selectedLayout = layoutFor(specifierMap(localEntries));
1280
- if (usesCompiler) {
1281
- if (layoutFor(specifierMap(localEntries.filter(({ section }) => section !== "peerDependencies"))) === void 0) selectedLayout = void 0;
1282
- if (selectedLayout === void 0) {
1283
- const workspace = workspaceLayoutFor(file);
1284
- const layout = workspace?.layout;
1285
- if (workspace !== void 0 && layout !== void 0) {
1286
- const expected = /* @__PURE__ */ new Map([[layout.compilerPackage, layout.compilerSpecifier]]);
1287
- if (layout.type === "split-compatibility") expected.set(layout.compatibilityPackage, layout.compatibilitySpecifier);
1288
- expected.set(policy.typescript6Compatibility.packageAlias, `npm:typescript@${policy.typescript6Compatibility.version}`);
1289
- const developmentOnly = localEntries.every(({ section, name, value }) => section === "devDependencies" && expected.get(name) === value);
1290
- const runtimeCompatibilityOnly = layout.type === "split-compatibility" && path.posix.dirname(file) !== workspace.directory && localEntries.every(({ section, name, value }) => (section === "dependencies" || section === "devDependencies" || section === "peerDependencies") && name === layout.compatibilityPackage && value === layout.compatibilitySpecifier);
1291
- if (developmentOnly || runtimeCompatibilityOnly) selectedLayout = layout;
1292
- }
1293
- }
1294
- if (selectedLayout === void 0) add({
1291
+ const command = record$7(json["scripts"]) ? json["scripts"]["typecheck"] : void 0;
1292
+ if (typeof command === "string") for (const words of compilerCommandSegments(command)) {
1293
+ if (!invokesCompiler(words)) continue;
1294
+ const normalized = words.join(" ");
1295
+ if (!words.some((word) => word === "(" || word === ")") && shellSubstitutions(normalized).length === 0 && (normalized === typecheckCommand || normalized.startsWith(`${typecheckCommand} `))) continue;
1296
+ add({
1295
1297
  file,
1296
1298
  rule: "typescript-layout",
1297
- key: "dependencies",
1298
- message: "TypeScript compiler and compatibility packages must match one complete declared install layout"
1299
+ key: "typecheck",
1300
+ value: command,
1301
+ message: `typecheck must run \`${typecheckCommand} --no-pretty --all --project=<tsconfig>\`, not a TypeScript compiler package`
1299
1302
  });
1300
- const command = record$7(json["scripts"]) ? json["scripts"]["typecheck"] : void 0;
1301
- if (selectedLayout !== void 0 && typeof command === "string") {
1302
- const normalizedExpected = selectedLayout.typecheckCommand.replace(/\s+/g, " ").trim();
1303
- for (const words of compilerCommandSegments(command)) {
1304
- if (!invokesCompiler(words)) continue;
1305
- const normalized = words.join(" ");
1306
- if (!words.some((word) => word === "(" || word === ")") && shellSubstitutions(normalized).length === 0 && (normalized === normalizedExpected || normalized.startsWith(`${normalizedExpected} `))) continue;
1307
- add({
1308
- file,
1309
- rule: "typescript-layout",
1310
- key: "typecheck",
1311
- value: command,
1312
- message: `typecheck compiler invocation must use ${selectedLayout.typecheckCommand}`
1313
- });
1314
- break;
1315
- }
1316
- }
1303
+ break;
1317
1304
  }
1318
- if (record$7(json["peerDependencies"])) {
1319
- const peerLayout = selectedLayout ?? workspaceLayoutFor(file)?.layout;
1320
- for (const [name, raw] of Object.entries(json["peerDependencies"])) {
1321
- if (!tsSpecifiers.has(name)) continue;
1322
- const range = resolve(file, name, raw);
1323
- if (!peerRange(range)) continue;
1324
- const releases = /* @__PURE__ */ new Set();
1325
- for (const layout of policy.typescriptInstallLayouts) {
1326
- if (layout.compilerPackage !== name && !(layout.type === "split-compatibility" && layout.compatibilityPackage === name)) continue;
1327
- const specifier = layout.compilerSpecifier;
1328
- releases.add(specifier.startsWith("npm:") ? specifier.slice(specifier.lastIndexOf("@") + 1) : specifier);
1329
- }
1330
- if (name === policy.typescript6Compatibility.packageAlias) releases.add(policy.typescript6Compatibility.version);
1331
- if (peerLayout?.type === "split-compatibility" && name === peerLayout.compatibilityPackage) releases.add(peerLayout.compatibilitySpecifier);
1332
- if ([...releases].every((release) => satisfies(release, range))) continue;
1305
+ if (record$7(json["peerDependencies"])) for (const [name, raw] of Object.entries(json["peerDependencies"])) {
1306
+ if (!typescriptPackages.has(name)) continue;
1307
+ const range = resolve(file, name, raw);
1308
+ if (!peerRange(range)) continue;
1309
+ const release = name === "typescript" ? policy.typescript : void 0;
1310
+ if (release === void 0) {
1333
1311
  add({
1334
1312
  file,
1335
1313
  rule: "typescript-layout",
1336
1314
  key: name,
1337
1315
  value: raw,
1338
- message: `${name} peer support range must include ${[...releases].join(" and ")}, found ${range}`
1316
+ message: typescriptMessage(name, range)
1339
1317
  });
1318
+ continue;
1340
1319
  }
1320
+ if (satisfies(release, range)) continue;
1321
+ add({
1322
+ file,
1323
+ rule: "typescript-layout",
1324
+ key: name,
1325
+ value: raw,
1326
+ message: `${name} peer support range must include ${release}, found ${range}`
1327
+ });
1341
1328
  }
1342
1329
  if (record$7(json["engines"]) && json["engines"]["node"] !== void 0 && (typeof json["engines"]["node"] !== "string" || !nodeSupportRangeMatches(policy.node, json["engines"]["node"]))) add({
1343
1330
  file,
@@ -1347,6 +1334,35 @@ const checkPackageFiles = ({ files, policy }) => {
1347
1334
  message: `engines.node must support ${policy.node}, found ${JSON.stringify(json["engines"]["node"])}`
1348
1335
  });
1349
1336
  }
1337
+ const staleness = (entry) => {
1338
+ const file = workspaceManifest(entry.workspace);
1339
+ const json = manifests.get(file);
1340
+ if (json === void 0) return `${file} is missing or invalid; remove the entry`;
1341
+ const declares = (name) => installSections.some((section) => {
1342
+ const entries = json[section];
1343
+ return record$7(entries) && Object.hasOwn(entries, name);
1344
+ });
1345
+ if (!declares(entry.name)) return `${file} no longer declares ${entry.name} in dependencies, devDependencies or optionalDependencies; remove the entry`;
1346
+ 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`;
1347
+ };
1348
+ for (const entry of typescriptAllowlist) {
1349
+ const stale = staleness(entry);
1350
+ if (stale === void 0) continue;
1351
+ const message = `typescriptAllowlist entry ${entry.name} ${entry.spec} for ${entry.workspace} is stale: ${stale}`;
1352
+ if (sources.has(allowlistFile)) add({
1353
+ file: allowlistFile,
1354
+ rule: "typescript-layout",
1355
+ key: "workspace",
1356
+ value: entry.workspace,
1357
+ message
1358
+ });
1359
+ else diagnostics.push({
1360
+ rule: "typescript-layout",
1361
+ path: allowlistFile,
1362
+ line: 1,
1363
+ message
1364
+ });
1365
+ }
1350
1366
  return diagnostics;
1351
1367
  };
1352
1368
  const extensionKinds = new Map(Object.entries({
@@ -1736,6 +1752,64 @@ const readCommand = ({ root, executable, args, runtime }) => {
1736
1752
  if (result.status !== 0) throw new Error(`configuration discovery failed: ${(result.stderr + result.stdout).trim()}`);
1737
1753
  return result.stdout;
1738
1754
  };
1755
+ const projectConfigScript = `
1756
+ const [moduleUrl, root, config] = process.argv.slice(1);
1757
+ const { API } = await import(moduleUrl);
1758
+ const api = new API({ cwd: root });
1759
+ try {
1760
+ const parsed = await api.parseConfigFile(config);
1761
+ // An empty files list (TS18002) or no inputs (TS18003) leaves a project
1762
+ // without roots; only unreadable configuration fails discovery.
1763
+ const errors = parsed.errors.filter((error) => error.code !== 18002 && error.code !== 18003);
1764
+ if (errors.length > 0) {
1765
+ process.stderr.write(errors.map((error) => "TS" + error.code + ": " + (error.text ?? error.messageText ?? "")).join("\\n"));
1766
+ process.exitCode = 1;
1767
+ } else
1768
+ process.stdout.write(JSON.stringify({
1769
+ files: parsed.fileNames,
1770
+ references: (parsed.projectReferences ?? []).map((reference) => reference.path),
1771
+ allowJs: parsed.options.allowJs === true,
1772
+ checkJs: parsed.options.checkJs === true,
1773
+ }));
1774
+ } finally {
1775
+ await api.close();
1776
+ }
1777
+ `;
1778
+ const resolveProjectApi = (root) => {
1779
+ for (const base of [import.meta.url, path.join(root, "package.json")]) try {
1780
+ return pathToFileURL(createRequire(base).resolve("tsrs/unstable/async")).href;
1781
+ } catch (error) {
1782
+ const code = record$4(error) ? error["code"] : void 0;
1783
+ if (code !== "MODULE_NOT_FOUND" && code !== "ERR_MODULE_NOT_FOUND") throw error;
1784
+ }
1785
+ throw new Error("configuration discovery needs tsrs: reinstall @stll/oxlint-config, which depends on it");
1786
+ };
1787
+ const readProjectConfig = (root, config) => {
1788
+ const api = resolveProjectApi(root);
1789
+ const result = spawnSync("node", [
1790
+ "--input-type=module",
1791
+ "-e",
1792
+ projectConfigScript,
1793
+ api,
1794
+ root,
1795
+ path.resolve(root, config)
1796
+ ], {
1797
+ cwd: root,
1798
+ encoding: "utf8",
1799
+ timeout: 3e4,
1800
+ maxBuffer: 33554432
1801
+ });
1802
+ if (result.error !== void 0) throw result.error;
1803
+ if (result.status !== 0) throw new Error(`configuration discovery failed: ${(result.stderr + result.stdout).trim()}`);
1804
+ const parsed = JSON.parse(result.stdout);
1805
+ 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");
1806
+ return {
1807
+ files: parsed["files"],
1808
+ references: parsed["references"],
1809
+ allowJs: parsed["allowJs"],
1810
+ checkJs: parsed["checkJs"]
1811
+ };
1812
+ };
1739
1813
  const relativePath = (root, file) => path.relative(root, path.resolve(root, file)).split(path.sep).join("/");
1740
1814
  const lifecycleFile = (root, file, tracked) => {
1741
1815
  try {
@@ -1834,7 +1908,6 @@ const checkNodeProjects = ({ root, manifest, trackedFiles }) => {
1834
1908
  try {
1835
1909
  const require = createRequire(path.join(root, "package.json"));
1836
1910
  const oxlint = path.join(path.dirname(require.resolve("oxlint/package.json")), "bin/oxlint");
1837
- const compiler = path.join(path.dirname(require.resolve("typescript/package.json")), "bin/tsc");
1838
1911
  const commands = inventory.commands.map((command) => ({
1839
1912
  cwd: root,
1840
1913
  ...command
@@ -1896,32 +1969,16 @@ const checkNodeProjects = ({ root, manifest, trackedFiles }) => {
1896
1969
  references: []
1897
1970
  };
1898
1971
  try {
1899
- const project = JSON.parse(readCommand({
1900
- root,
1901
- executable: compiler,
1902
- args: [
1903
- "--showConfig",
1904
- "--project",
1905
- config
1906
- ],
1907
- runtime: "node"
1908
- }));
1909
- if (!record$4(project)) throw new Error("compiler configuration must be an object");
1910
- const references = project["references"] ?? [];
1911
- if (!Array.isArray(references)) throw new Error("compiler configuration references must be an array");
1912
- for (const reference of references) {
1913
- if (!record$4(reference) || typeof reference["path"] !== "string") throw new Error("compiler project reference requires a path");
1914
- const target = relativePath(root, path.resolve(root, path.dirname(config), reference["path"]));
1972
+ const project = readProjectConfig(root, config);
1973
+ for (const reference of project.references) {
1974
+ const target = relativePath(root, reference);
1915
1975
  const referencedConfig = target.endsWith(".json") ? target : path.posix.join(target, "tsconfig.json");
1916
1976
  if (!trackedFiles.has(referencedConfig)) throw new Error(`project reference is not tracked: ${referencedConfig}`);
1917
1977
  result.references.push(referencedConfig);
1918
1978
  }
1919
- const sources = project["files"] ?? [];
1920
- if (!stringList(sources)) throw new Error("compiler configuration did not return a files array");
1921
- const options = project["compilerOptions"];
1922
- for (const file of sources) {
1923
- if (scriptSourceKind(file) === "javascript" && (!record$4(options) || options["allowJs"] !== true && options["checkJs"] !== true)) continue;
1924
- result.files.add(relativePath(root, path.resolve(root, path.dirname(config), file)));
1979
+ for (const file of project.files) {
1980
+ if (scriptSourceKind(file) === "javascript" && !project.allowJs && !project.checkJs) continue;
1981
+ result.files.add(relativePath(root, file));
1925
1982
  }
1926
1983
  } catch (error) {
1927
1984
  result.files.clear();
@@ -1969,8 +2026,11 @@ const checkNodeProjects = ({ root, manifest, trackedFiles }) => {
1969
2026
  //#endregion
1970
2027
  //#region src/toolchain-rust.ts
1971
2028
  const record$3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1972
- /** Exceptions authorize one immutable release, never a channel or an entire rule. */
1973
- const parseRustExceptions = (input, today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)) => {
2029
+ /**
2030
+ * Exceptions authorize one immutable release, never a channel or an entire
2031
+ * rule. Expiry is scoped separately by checkRustExceptionExpiry.
2032
+ */
2033
+ const parseRustExceptions = (input) => {
1974
2034
  if (input === void 0) return [];
1975
2035
  if (!Array.isArray(input)) throw new Error("rustExceptions must be an array");
1976
2036
  const pins = /* @__PURE__ */ new Set();
@@ -1980,7 +2040,7 @@ const parseRustExceptions = (input, today = (/* @__PURE__ */ new Date()).toISOSt
1980
2040
  "reason",
1981
2041
  "tracking",
1982
2042
  "expires"
1983
- ].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");
2043
+ ].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");
1984
2044
  pins.add(entry["pin"]);
1985
2045
  return {
1986
2046
  pin: entry["pin"],
@@ -1990,6 +2050,30 @@ const parseRustExceptions = (input, today = (/* @__PURE__ */ new Date()).toISOSt
1990
2050
  };
1991
2051
  });
1992
2052
  };
2053
+ /** Whether the base stll-toolchain.json declares this exception with the same expiry. */
2054
+ const declaresException = (baseText, { pin, expires }) => {
2055
+ const base = JSON.parse(baseText);
2056
+ const exceptions = record$3(base) ? base["rustExceptions"] : void 0;
2057
+ return Array.isArray(exceptions) && exceptions.some((entry) => record$3(entry) && entry["pin"] === pin && entry["expires"] === expires);
2058
+ };
2059
+ /** An exception expires on its date; the scope decides whether that fails this run. */
2060
+ const checkRustExceptionExpiry = ({ exceptions, now = /* @__PURE__ */ new Date(), scope }) => {
2061
+ const today = now.toISOString().slice(0, 10);
2062
+ const errors = [];
2063
+ const warnings = [];
2064
+ for (const exception of exceptions) {
2065
+ if (exception.expires > today) continue;
2066
+ const overdue = `stll-toolchain.json Rust exception ${exception.pin} expired on ${exception.expires}`;
2067
+ const verdict = overdueVerdict(scope, "stll-toolchain.json", (base) => declaresException(base, exception));
2068
+ 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)`);
2069
+ else if (verdict.unavailable === void 0) errors.push(`${overdue}; remove it or move to the shared Rust pin`);
2070
+ else errors.push(`${overdue}; cannot tell whether this change adds it: ${verdict.unavailable}`);
2071
+ }
2072
+ return {
2073
+ errors,
2074
+ warnings
2075
+ };
2076
+ };
1993
2077
  /** Only executable fields are inspected: unrelated action inputs are not Rust selectors. */
1994
2078
  const checkRustReferences = (options) => {
1995
2079
  const { file, rust, exceptions } = options;
@@ -3892,7 +3976,7 @@ const record = (value) => typeof value === "object" && value !== null && !Array.
3892
3976
  const array = (value) => Array.isArray(value);
3893
3977
  /** Repository-owned declarations are explicit; malformed decisions fail closed. */
3894
3978
  const parseToolchainConfiguration = (input) => {
3895
- 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");
3979
+ 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");
3896
3980
  const disabled = /* @__PURE__ */ new Set();
3897
3981
  const optOuts = input["optOuts"] ?? [];
3898
3982
  if (!array(optOuts)) throw new Error("optOuts must be an array");
@@ -3913,7 +3997,8 @@ const parseToolchainConfiguration = (input) => {
3913
3997
  engineFloors: parseEngineFloors(input["engineFloors"]),
3914
3998
  dynamicSelectors: parseDynamicSelectors(input["dynamicSelectors"]),
3915
3999
  consumerChecks: parseConsumerChecks(input["consumerChecks"]),
3916
- dependabotIgnores: parseDependabotIgnores(input["dependabotIgnores"])
4000
+ dependabotIgnores: parseDependabotIgnores(input["dependabotIgnores"]),
4001
+ typescriptAllowlist: parseTypescriptAllowlist(input["typescriptAllowlist"])
3917
4002
  };
3918
4003
  };
3919
4004
  /** Dependency graphs stay opaque unless a caller explicitly needs registry identities. */
@@ -3971,8 +4056,12 @@ const readToolchainInputs = (root, { bunLock = "presence" } = {}) => {
3971
4056
  diagnostics
3972
4057
  };
3973
4058
  };
3974
- const checkToolchain = ({ root, policy, onDelegated, onWarning, now }) => {
4059
+ const checkToolchain = ({ root, policy: sharedPolicy, onDelegated, onWarning, now, expiryScope = { kind: "enforce" } }) => {
3975
4060
  const { files, trackedFiles, diagnostics } = readToolchainInputs(root, { bunLock: "content" });
4061
+ const policy = {
4062
+ ...sharedPolicy,
4063
+ bun: repositoryBun(sharedPolicy.bun, files["package.json"])
4064
+ };
3976
4065
  let repository;
3977
4066
  try {
3978
4067
  const origin = execFileSync("git", [
@@ -3996,6 +4085,23 @@ const checkToolchain = ({ root, policy, onDelegated, onWarning, now }) => {
3996
4085
  if (files["stll-toolchain.json"] !== void 0) try {
3997
4086
  configuration = parseToolchainConfiguration(JSON.parse(files["stll-toolchain.json"]));
3998
4087
  dynamicSelectors = configuration.dynamicSelectors;
4088
+ const expiry = checkRustExceptionExpiry({
4089
+ exceptions: configuration.rustExceptions,
4090
+ now,
4091
+ scope: expiryScope
4092
+ });
4093
+ for (const message of expiry.errors) diagnostics.push({
4094
+ rule: "configuration",
4095
+ path: "stll-toolchain.json",
4096
+ line: 1,
4097
+ message
4098
+ });
4099
+ for (const message of expiry.warnings) onWarning?.({
4100
+ rule: "configuration",
4101
+ path: "stll-toolchain.json",
4102
+ line: 1,
4103
+ message
4104
+ });
3999
4105
  for (const entry of configuration.engineFloors) engineFloors.push(resolveEngineFloor(entry, files));
4000
4106
  } catch (error) {
4001
4107
  diagnostics.push({
@@ -4060,7 +4166,8 @@ const checkToolchain = ({ root, policy, onDelegated, onWarning, now }) => {
4060
4166
  }));
4061
4167
  diagnostics.push(...checkPackageFiles({
4062
4168
  files,
4063
- policy
4169
+ policy,
4170
+ typescriptAllowlist: configuration.typescriptAllowlist
4064
4171
  }));
4065
4172
  const usesBun = Object.entries(files).some(([file, text]) => {
4066
4173
  if (path.posix.basename(file) === "bun.lock") return true;
@@ -4082,7 +4189,8 @@ const checkToolchain = ({ root, policy, onDelegated, onWarning, now }) => {
4082
4189
  const result = checkRepositoryQuarantine({
4083
4190
  files,
4084
4191
  now,
4085
- npmrcPresent: trackedFiles.has(".npmrc")
4192
+ npmrcPresent: trackedFiles.has(".npmrc"),
4193
+ expiry: expiryScope
4086
4194
  });
4087
4195
  for (const message of result.errors) diagnostics.push({
4088
4196
  rule: "quarantine-policy",
@@ -4168,6 +4276,6 @@ const checkToolchain = ({ root, policy, onDelegated, onWarning, now }) => {
4168
4276
  return diagnostics.filter((diagnostic) => !configuration.disabled.has(diagnostic.rule));
4169
4277
  };
4170
4278
  //#endregion
4171
- export { parseToolchainConfiguration as n, readToolchainInputs as r, checkToolchain as t };
4279
+ export { repositoryBun as i, parseToolchainConfiguration as n, readToolchainInputs as r, checkToolchain as t };
4172
4280
 
4173
4281
  //# sourceMappingURL=toolchain-guard.mjs.map