@geonosis/doctor 1.4.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.
@@ -322,13 +322,15 @@ var checkDeployed = ({ root }) => {
322
322
  // src/types.ts
323
323
  var CHECKS = [
324
324
  "loaded",
325
+ "group",
325
326
  "exercised",
326
327
  "baseline",
327
328
  "runner",
328
329
  "envelope",
329
330
  "drift",
330
331
  "observability",
331
- "deployed"
332
+ "deployed",
333
+ "rails"
332
334
  ];
333
335
  var DoctorError = class extends Error {
334
336
  constructor(message) {
@@ -405,6 +407,20 @@ var probesOf = async (entry, plugin) => {
405
407
  })
406
408
  );
407
409
  };
410
+ var presumptionsOf = async (entry) => {
411
+ const loaded = await import(pathToFileURL(entry).href);
412
+ const namespace = loaded.default?.meta?.name;
413
+ return {
414
+ namespace: typeof namespace === "string" ? namespace : "",
415
+ presumed: Object.fromEntries(
416
+ Object.entries(loaded.default?.rules ?? {}).flatMap(([name, rule]) => {
417
+ const presumes = rule?.presumes;
418
+ const engine = presumes?.engine;
419
+ return typeof engine === "string" && Array.isArray(presumes?.packages) ? [[name, { engine, packages: presumes.packages }]] : [];
420
+ })
421
+ )
422
+ };
423
+ };
408
424
  var corpusOfPlugin = (from, specifier) => join3(packageDirOf(resolveFrom(from, specifier), specifier), "corpus");
409
425
  var real = (path) => {
410
426
  try {
@@ -416,9 +432,40 @@ var real = (path) => {
416
432
  var relativeToRoot = (root, path) => relativePath(real(root), real(path));
417
433
 
418
434
  // src/drift.ts
419
- import { closeSync, existsSync as existsSync3, openSync, readdirSync as readdirSync2, readFileSync as readFileSync4, readSync } from "fs";
435
+ import { closeSync, existsSync as existsSync4, openSync, readdirSync as readdirSync3, readFileSync as readFileSync5, readSync } from "fs";
420
436
  import { homedir } from "os";
421
- import { join as join4, sep as sep2 } from "path";
437
+ import { join as join5, sep as sep2 } from "path";
438
+
439
+ // src/hooks.ts
440
+ import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
441
+ import { join as join4 } from "path";
442
+ var HOOK_FILES = ["lefthook.yml", "lefthook.yaml", ".lefthook.yml", ".lefthook.yaml"];
443
+ var HOOK_DIRS = [".husky", ".githooks"];
444
+ var VERSION_MANAGED = /(?:^|[\s;&|(])(?<runner>pnpm|npx|bunx|yarn)\b(?:\s+(?:exec|run|dlx|x))?\s+(?<bin>geonosis(?:-[a-z-]+)?)\b/;
445
+ var NAMES_A_BIN = /(?:^|[\s;&|(/])geonosis(?:-[a-z-]+)?\b/;
446
+ var hookLines = (root) => {
447
+ const found = [];
448
+ const read = (path) => {
449
+ const at = relativePath(root, path);
450
+ for (const line of readFileSync4(path, "utf8").split("\n")) {
451
+ if (line.trim() !== "") found.push({ at, line });
452
+ }
453
+ };
454
+ for (const name of HOOK_FILES) {
455
+ const path = join4(root, name);
456
+ if (existsSync3(path)) read(path);
457
+ }
458
+ for (const name of HOOK_DIRS) {
459
+ const dir = join4(root, name);
460
+ if (!existsSync3(dir)) continue;
461
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
462
+ if (entry.isFile() && !entry.name.startsWith("_") && !entry.name.startsWith(".")) {
463
+ read(join4(dir, entry.name));
464
+ }
465
+ }
466
+ }
467
+ return found;
468
+ };
422
469
 
423
470
  // src/overrides.ts
424
471
  var SPECIAL = /* @__PURE__ */ new Set(["$", "(", ")", "+", ".", "/", "@", "\\", "^", "|"]);
@@ -502,29 +549,29 @@ var filesUnder = (dir, match) => {
502
549
  const walk2 = (at) => {
503
550
  let entries;
504
551
  try {
505
- entries = readdirSync2(at, { withFileTypes: true });
552
+ entries = readdirSync3(at, { withFileTypes: true });
506
553
  } catch {
507
554
  return;
508
555
  }
509
556
  for (const entry of entries) {
510
557
  if (entry.isDirectory()) {
511
- if (!entry.name.startsWith(".") && !NEVER_WALKED2.has(entry.name)) walk2(join4(at, entry.name));
558
+ if (!entry.name.startsWith(".") && !NEVER_WALKED2.has(entry.name)) walk2(join5(at, entry.name));
512
559
  continue;
513
560
  }
514
- if (entry.isFile() && match(entry.name)) found.push(join4(at, entry.name));
561
+ if (entry.isFile() && match(entry.name)) found.push(join5(at, entry.name));
515
562
  }
516
563
  };
517
564
  walk2(dir);
518
565
  return found;
519
566
  };
520
567
  var ci = (root) => {
521
- const dir = join4(root, WORKFLOWS);
522
- if (!existsSync3(dir)) {
568
+ const dir = join5(root, WORKFLOWS);
569
+ if (!existsSync4(dir)) {
523
570
  return [finding3(WORKFLOWS, "SKIP", "there are no workflows here to read")];
524
571
  }
525
572
  return filesUnder(dir, (name) => name.endsWith(".yml") || name.endsWith(".yaml")).map((path) => {
526
573
  const at = relativePath(root, path);
527
- return SWITCHED_OFF.test(readFileSync4(path, "utf8")) ? finding3(
574
+ return SWITCHED_OFF.test(readFileSync5(path, "utf8")) ? finding3(
528
575
  at,
529
576
  "FAIL",
530
577
  "a job or step here is switched off by a condition that can never be true \u2014 every gate downstream of it reports green having run nothing"
@@ -572,7 +619,7 @@ var pathsRunByScript = (script) => script.split(BETWEEN_COMMANDS).map(pathRunBy)
572
619
  var scriptPaths = (workspaces) => {
573
620
  const missing = workspaces.flatMap(
574
621
  (one) => Object.entries(one.manifest.scripts ?? {}).flatMap(
575
- ([name, script]) => pathsRunByScript(script).filter((path) => !existsSync3(join4(one.dir, path))).map((path) => ({ name, path, workspace: one }))
622
+ ([name, script]) => pathsRunByScript(script).filter((path) => !existsSync4(join5(one.dir, path))).map((path) => ({ name, path, workspace: one }))
576
623
  )
577
624
  );
578
625
  if (missing.length === 0) {
@@ -608,7 +655,7 @@ var linkedBins = (root, workspaces) => {
608
655
  }
609
656
  const missing = workspaces.flatMap(
610
657
  (one) => Object.entries(one.manifest.scripts ?? {}).flatMap(
611
- ([script, body]) => [...new Set(body.split(/[\s;|&()]+/).filter((word) => bins.has(word)))].filter((name) => !existsSync3(join4(root, "node_modules/.bin", name))).map((name) => ({ name, script, workspace: one }))
658
+ ([script, body]) => [...new Set(body.split(/[\s;|&()]+/).filter((word) => bins.has(word)))].filter((name) => !existsSync4(join5(root, "node_modules/.bin", name))).map((name) => ({ name, script, workspace: one }))
612
659
  )
613
660
  );
614
661
  if (missing.length === 0) {
@@ -679,11 +726,11 @@ var everythingRun = (root, workspaces) => {
679
726
  const scripts = workspaces.flatMap((one) => Object.values(one.manifest.scripts ?? {}));
680
727
  const counters = (readRatchet(root)?.counters ?? []).map((entry) => String(entry.command ?? ""));
681
728
  const workflows = filesUnder(
682
- join4(root, WORKFLOWS),
729
+ join5(root, WORKFLOWS),
683
730
  (name) => name.endsWith(".yml") || name.endsWith(".yaml")
684
731
  ).map((path) => {
685
732
  try {
686
- return readFileSync4(path, "utf8");
733
+ return readFileSync5(path, "utf8");
687
734
  } catch {
688
735
  return "";
689
736
  }
@@ -719,10 +766,10 @@ var generatedFiles = (root, workspaces) => {
719
766
  });
720
767
  };
721
768
  var readGeonosis = (root) => {
722
- const path = join4(root, GEONOSIS);
723
- if (!existsSync3(path)) return void 0;
769
+ const path = join5(root, GEONOSIS);
770
+ if (!existsSync4(path)) return void 0;
724
771
  try {
725
- return JSON.parse(readFileSync4(path, "utf8"));
772
+ return JSON.parse(readFileSync5(path, "utf8"));
726
773
  } catch {
727
774
  return void 0;
728
775
  }
@@ -730,11 +777,11 @@ var readGeonosis = (root) => {
730
777
  var law = (root, config) => {
731
778
  const declared = config?.law ?? {};
732
779
  const file = typeof declared.file === "string" ? declared.file : LAW;
733
- const path = join4(root, file);
734
- if (!existsSync3(path)) {
780
+ const path = join5(root, file);
781
+ if (!existsSync4(path)) {
735
782
  return [finding3(file, "SKIP", "there is no law file here to measure")];
736
783
  }
737
- const source = readFileSync4(path, "utf8");
784
+ const source = readFileSync5(path, "utf8");
738
785
  const lines = source.split("\n").length - (source.endsWith("\n") ? 1 : 0);
739
786
  if (typeof declared.maxLines !== "number") {
740
787
  return [
@@ -756,9 +803,9 @@ var law = (root, config) => {
756
803
  };
757
804
  var KIT_PLUGIN = "geonosis";
758
805
  var enablesKit = (path) => {
759
- if (!existsSync3(path)) return false;
806
+ if (!existsSync4(path)) return false;
760
807
  try {
761
- const parsed = JSON.parse(readFileSync4(path, "utf8"));
808
+ const parsed = JSON.parse(readFileSync5(path, "utf8"));
762
809
  const enabled = parsed.enabledPlugins;
763
810
  if (typeof enabled !== "object" || enabled === null) return false;
764
811
  return Object.entries(enabled).some(
@@ -769,7 +816,7 @@ var enablesKit = (path) => {
769
816
  }
770
817
  };
771
818
  var hooks = (root, userSettings) => {
772
- const path = join4(root, SETTINGS);
819
+ const path = join5(root, SETTINGS);
773
820
  if (enablesKit(path)) {
774
821
  return [finding3(SETTINGS, "OK", `\`enabledPlugins\` here enables the kit\u2019s plugin`)];
775
822
  }
@@ -790,6 +837,160 @@ var hooks = (root, userSettings) => {
790
837
  )
791
838
  ];
792
839
  };
840
+ var ALLOW_BUILDS_KEY = "allowBuilds";
841
+ var allowedBuilds = (root) => {
842
+ const path = join5(root, WORKSPACE_YAML);
843
+ if (!existsSync4(path)) return void 0;
844
+ const lines = readFileSync5(path, "utf8").split("\n");
845
+ const at = lines.findIndex((line) => new RegExp(`^${ALLOW_BUILDS_KEY}\\s*:`).test(line));
846
+ if (at < 0) return void 0;
847
+ const names = [];
848
+ for (const line of lines.slice(at + 1)) {
849
+ if (/^\s*(?:#.*)?$/.test(line)) continue;
850
+ const entry = /^\s+(?<name>'[^']+'|"[^"]+"|[^:\s]+)\s*:/.exec(line);
851
+ const name = entry?.groups?.["name"];
852
+ if (name === void 0) break;
853
+ names.push(name.replace(/^['"]|['"]$/g, ""));
854
+ }
855
+ return names;
856
+ };
857
+ var PLATFORM = /(?:^|[-/])(?:darwin|linux|win32|windows|freebsd|openbsd|android|sunos)(?:[-.]|$)|(?:^|[-/])(?:arm64|x64|ia32|ppc64|s390x|riscv64)(?:[-.]|$)/;
858
+ var familyOf = (name) => name.split("/").slice(0, -1).join("/") || name;
859
+ var platformSplitBuilds = (root) => {
860
+ const names = allowedBuilds(root);
861
+ if (names === void 0 || names.length === 0) return [];
862
+ const families = /* @__PURE__ */ new Map();
863
+ for (const name of names.filter((one) => PLATFORM.test(one))) {
864
+ const family = familyOf(name);
865
+ families.set(family, [...families.get(family) ?? [], name]);
866
+ }
867
+ return [...families.entries()].flatMap(
868
+ ([family, allowed]) => allowed.length > 1 ? [] : [
869
+ finding3(
870
+ allowed[0] ?? family,
871
+ "WARN",
872
+ `this ${ALLOW_BUILDS_KEY} entry names one platform of ${family}, which publishes one package per platform \u2014 pnpm writes the entry for the machine that ran the install, so an install on any other platform refuses by the name that is missing, on somebody else's machine. List every platform ${family} publishes, or none of them`
873
+ )
874
+ ]
875
+ );
876
+ };
877
+ var LINTS_STAGED = /\b(?:oxlint|eslint|biome)\b[^\n]*(?:\{staged_files\}|\{files\}|\$\{?[@*]|"\$@")/;
878
+ var TAKES_A_VALUE = /* @__PURE__ */ new Set([
879
+ "--config",
880
+ "--format",
881
+ "--ignore-path",
882
+ "--max-warnings",
883
+ "--reporter",
884
+ "--rulesdir",
885
+ "--tsconfig",
886
+ "-c"
887
+ ]);
888
+ var pathsLintedBy = (script, dir) => {
889
+ const words = script.split(/\s+/).filter((word) => word !== "");
890
+ const named2 = [];
891
+ for (const [at, word] of words.entries()) {
892
+ if (word.startsWith("-")) continue;
893
+ const before = words[at - 1] ?? "";
894
+ if (TAKES_A_VALUE.has(before)) continue;
895
+ if (existsSync4(join5(dir, word))) named2.push(word.replace(/^\.\//, "").replace(/\/$/, ""));
896
+ }
897
+ return named2;
898
+ };
899
+ var LINTABLE = /\.[cm]?[jt]sx?$/;
900
+ var sourceDirsIn = (dir) => {
901
+ let entries;
902
+ try {
903
+ entries = readdirSync3(dir, { withFileTypes: true });
904
+ } catch {
905
+ return [];
906
+ }
907
+ return entries.filter(
908
+ (entry) => entry.isDirectory() && !entry.name.startsWith(".") && !NEVER_WALKED2.has(entry.name) && filesUnder(join5(dir, entry.name), (name) => LINTABLE.test(name)).length > 0
909
+ ).map((entry) => entry.name);
910
+ };
911
+ var lintScopes = (root, workspaces) => {
912
+ if (!hookLines(root).some((one) => LINTS_STAGED.test(one.line))) {
913
+ return [
914
+ finding3(
915
+ "lint scope",
916
+ "SKIP",
917
+ "no committed hook lints staged files here, so there is no second file set for a lint script to be narrower than"
918
+ )
919
+ ];
920
+ }
921
+ return workspaces.flatMap((workspace) => {
922
+ const script = workspace.manifest.scripts?.lint;
923
+ if (typeof script !== "string" || script.trim() === "") return [];
924
+ const at = workspace.relative === "" ? "package.json" : `${workspace.relative}/package.json`;
925
+ const covered = pathsLintedBy(script, workspace.dir);
926
+ if (covered.length === 0 || covered.includes(".")) {
927
+ return [finding3(at, "OK", `"${script}" covers everything the hook could stage here`)];
928
+ }
929
+ const uncovered = sourceDirsIn(workspace.dir).filter(
930
+ (name) => !covered.some((path) => name === path || path.startsWith(`${name}/`))
931
+ );
932
+ if (uncovered.length === 0) {
933
+ return [finding3(at, "OK", `"${script}" covers every source directory under this workspace`)];
934
+ }
935
+ return [
936
+ finding3(
937
+ at,
938
+ "WARN",
939
+ `the "lint" script covers ${covered.join(", ")} and the pre-commit hook lints whatever is staged, so ${uncovered.map((name) => `${name}/**`).join(", ")} is linted by the hook alone \u2014 the package gate has never read those files, and the drift stays invisible until one moves across it. Widen the script, or say in it which globs are deliberately outside`
940
+ )
941
+ ];
942
+ });
943
+ };
944
+ var OBSERVABILITY = "@geonosis/observability";
945
+ var COMPOSITION_ROOT = "src/platform.ts";
946
+ var FLOOR_PACKAGES = [
947
+ "@geonosis/conformance",
948
+ "@geonosis/db",
949
+ "@geonosis/events",
950
+ "@geonosis/search",
951
+ "@geonosis/workflows"
952
+ ];
953
+ var COMPOSABLE = [
954
+ {
955
+ name: OBSERVABILITY,
956
+ uncomposed: `a sink nobody composes at the composition root sends production errors nowhere, and from outside the process that reads exactly like a project with nothing to report. Build one where the app is wired (the recipe is in ${OBSERVABILITY}'s README, \xA74) and hand it the port`
957
+ },
958
+ ...FLOOR_PACKAGES.map((name) => ({
959
+ name,
960
+ uncomposed: `a battery nothing composes is behaviour no request path can reach, and an install that does nothing reads from outside exactly like one that works. Compose it in the app\u2019s ${COMPOSITION_ROOT} \u2014 the composition root \`create-geonosis --domains\` writes \u2014 where the tenant session is opened before any store is built`
961
+ }))
962
+ ];
963
+ var SOURCE_FILE = /\.[cm]?[jt]sx?$/;
964
+ var composedHere = (root, workspaces) => {
965
+ const declared = declaredAnywhere(workspaces);
966
+ const wanted = COMPOSABLE.filter((one) => declared.has(one.name));
967
+ if (wanted.length === 0) return [];
968
+ const importing = /* @__PURE__ */ new Map();
969
+ for (const path of filesUnder(root, (name) => SOURCE_FILE.test(name))) {
970
+ let body;
971
+ try {
972
+ body = readFileSync5(path, "utf8");
973
+ } catch {
974
+ continue;
975
+ }
976
+ for (const one of wanted) {
977
+ if (!importing.has(one.name) && body.includes(one.name)) importing.set(one.name, path);
978
+ }
979
+ if (importing.size === wanted.length) break;
980
+ }
981
+ return wanted.map((one) => {
982
+ const at = importing.get(one.name);
983
+ return at === void 0 ? finding3(
984
+ one.name,
985
+ "WARN",
986
+ `it is declared here and no source file imports it \u2014 ${one.uncomposed}`
987
+ ) : finding3(
988
+ one.name,
989
+ "OK",
990
+ `composed in ${relativePath(root, at)} \u2014 something here builds what it reports through`
991
+ );
992
+ });
993
+ };
793
994
  var READERS = {
794
995
  ledger: "@geonosis/ledger",
795
996
  review: "@geonosis/review",
@@ -882,8 +1083,8 @@ var pluginDirsOf = (level) => {
882
1083
  return { manifests: options.manifests ?? ["index.ts"], registry, roots: options.roots };
883
1084
  };
884
1085
  var pluginDirLayers = (root) => {
885
- const path = join4(root, ".oxlintrc.json");
886
- if (!existsSync3(path)) return [];
1086
+ const path = join5(root, ".oxlintrc.json");
1087
+ if (!existsSync4(path)) return [];
887
1088
  const layers = [];
888
1089
  for (const layer of layersOf(readConfig(path, root), PLUGIN_DIR_RULE)) {
889
1090
  const dirs = pluginDirsOf(layer.level);
@@ -892,8 +1093,8 @@ var pluginDirLayers = (root) => {
892
1093
  return layers;
893
1094
  };
894
1095
  var layerOver = (layers, root, relative) => {
895
- const at = join4(root, relative);
896
- const inside = readdirSync2(at, { withFileTypes: true }).filter((entry) => entry.isFile()).map((entry) => `${relative}/${entry.name}`);
1096
+ const at = join5(root, relative);
1097
+ const inside = readdirSync3(at, { withFileTypes: true }).filter((entry) => entry.isFile()).map((entry) => `${relative}/${entry.name}`);
897
1098
  const paths = inside.length === 0 ? [relative] : inside;
898
1099
  const wrapped = layers.map((layer) => ({ files: layer.files, level: layer.dirs }));
899
1100
  for (const path of paths) {
@@ -914,7 +1115,7 @@ var pluginDirs = (root) => {
914
1115
  ];
915
1116
  }
916
1117
  const missing = [...new Set(layers.flatMap((layer) => layer.dirs.registry))].filter(
917
- (registry) => !existsSync3(join4(root, registry))
1118
+ (registry) => !existsSync4(join5(root, registry))
918
1119
  );
919
1120
  if (missing.length > 0) {
920
1121
  return missing.map(
@@ -924,17 +1125,17 @@ var pluginDirs = (root) => {
924
1125
  const source = /* @__PURE__ */ new Map();
925
1126
  const unreachable = /* @__PURE__ */ new Map();
926
1127
  for (const rootDir of new Set(layers.flatMap((layer) => layer.dirs.roots))) {
927
- const at = join4(root, rootDir);
928
- if (!existsSync3(at)) continue;
929
- for (const entry of readdirSync2(at, { withFileTypes: true })) {
1128
+ const at = join5(root, rootDir);
1129
+ if (!existsSync4(at)) continue;
1130
+ for (const entry of readdirSync3(at, { withFileTypes: true })) {
930
1131
  if (!entry.isDirectory()) continue;
931
1132
  const relative = `${rootDir}/${entry.name}`;
932
1133
  const governs = layerOver(layers, root, relative);
933
1134
  if (governs === void 0) continue;
934
1135
  const key = governs.registry.join(", ");
935
- const registry = source.get(key) ?? governs.registry.map((half) => readFileSync4(join4(root, half), "utf8")).join("\n");
1136
+ const registry = source.get(key) ?? governs.registry.map((half) => readFileSync5(join5(root, half), "utf8")).join("\n");
936
1137
  source.set(key, registry);
937
- const hasManifest = governs.manifests.some((name) => existsSync3(join4(at, entry.name, name)));
1138
+ const hasManifest = governs.manifests.some((name) => existsSync4(join5(at, entry.name, name)));
938
1139
  if (!hasManifest || registry.includes(entry.name)) continue;
939
1140
  unreachable.set(key, [...unreachable.get(key) ?? [], relative]);
940
1141
  }
@@ -952,9 +1153,9 @@ var WORKSPACE_YAML = "pnpm-workspace.yaml";
952
1153
  var HOIST_KEY = "publicHoistPattern";
953
1154
  var HOIST_REPAIR = "rm -rf node_modules/.modules.yaml node_modules/.pnpm-workspace-state-v1.json && pnpm install";
954
1155
  var hoistPatterns = (root) => {
955
- const path = join4(root, WORKSPACE_YAML);
956
- if (!existsSync3(path)) return void 0;
957
- const lines = readFileSync4(path, "utf8").split("\n");
1156
+ const path = join5(root, WORKSPACE_YAML);
1157
+ if (!existsSync4(path)) return void 0;
1158
+ const lines = readFileSync5(path, "utf8").split("\n");
958
1159
  const at = lines.findIndex((line) => new RegExp(`^${HOIST_KEY}\\s*:`).test(line));
959
1160
  if (at < 0) return void 0;
960
1161
  const patterns = [];
@@ -991,7 +1192,7 @@ var publicHoists = (root, workspaces) => {
991
1192
  `this ${HOIST_KEY} matches no workspace package here, so what it should have linked at the root is a question this cannot answer`
992
1193
  );
993
1194
  }
994
- const pruned = hoisted.filter((name) => !existsSync3(join4(root, "node_modules", name)));
1195
+ const pruned = hoisted.filter((name) => !existsSync4(join5(root, "node_modules", name)));
995
1196
  if (pruned.length === 0) {
996
1197
  return finding3(
997
1198
  pattern,
@@ -1006,71 +1207,10 @@ var publicHoists = (root, workspaces) => {
1006
1207
  );
1007
1208
  });
1008
1209
  };
1009
- var HOOK_FILES = ["lefthook.yml", "lefthook.yaml", ".lefthook.yml", ".lefthook.yaml"];
1010
- var HOOK_DIRS = [".husky", ".githooks"];
1011
- var VERSION_MANAGED = /(?:^|[\s;&|(])(?<runner>pnpm|npx|bunx|yarn)\b(?:\s+(?:exec|run|dlx|x))?\s+(?<bin>geonosis(?:-[a-z-]+)?)\b/;
1012
- var NAMES_A_BIN = /(?:^|[\s;&|(/])geonosis(?:-[a-z-]+)?\b/;
1013
- var hookLines = (root) => {
1014
- const found = [];
1015
- const read = (path) => {
1016
- const at = relativePath(root, path);
1017
- for (const line of readFileSync4(path, "utf8").split("\n")) {
1018
- if (line.trim() !== "") found.push({ at, line });
1019
- }
1020
- };
1021
- for (const name of HOOK_FILES) {
1022
- const path = join4(root, name);
1023
- if (existsSync3(path)) read(path);
1024
- }
1025
- for (const name of HOOK_DIRS) {
1026
- const dir = join4(root, name);
1027
- if (!existsSync3(dir)) continue;
1028
- for (const entry of readdirSync2(dir, { withFileTypes: true })) {
1029
- if (entry.isFile() && !entry.name.startsWith("_") && !entry.name.startsWith(".")) {
1030
- read(join4(dir, entry.name));
1031
- }
1032
- }
1033
- }
1034
- return found;
1035
- };
1036
- var gitHooks = (root) => {
1037
- const lines = hookLines(root);
1038
- if (lines.length === 0) {
1039
- return [finding3("git hooks", "SKIP", "no lefthook, husky or .githooks file here to read")];
1040
- }
1041
- const files = [...new Set(lines.map((one) => one.at))].toSorted();
1042
- return files.flatMap((at) => {
1043
- const here = lines.filter((one) => one.at === at);
1044
- const named2 = here.filter((one) => NAMES_A_BIN.test(one.line));
1045
- if (named2.length === 0) {
1046
- return [finding3(at, "SKIP", "it names no geonosis bin, so there is nothing here to start")];
1047
- }
1048
- const wrong = named2.flatMap((one) => {
1049
- const found = VERSION_MANAGED.exec(one.line)?.groups;
1050
- return found === void 0 ? [] : [{ bin: found["bin"] ?? "", runner: found["runner"] ?? "" }];
1051
- });
1052
- if (wrong.length === 0) {
1053
- return [
1054
- finding3(
1055
- at,
1056
- "OK",
1057
- `${named2.length} geonosis bin(s) here, every one called directly rather than through a runner`
1058
- )
1059
- ];
1060
- }
1061
- return wrong.map(
1062
- ({ bin, runner }) => finding3(
1063
- at,
1064
- "FAIL",
1065
- `it runs ${bin} through ${runner} \u2014 a git hook's PATH is not the shell's and carries no version-manager shim, so ${runner} fails to START and what the author reads is its message, not this gate's. Call it directly: node_modules/.bin/${bin}`
1066
- )
1067
- );
1068
- });
1069
- };
1070
1210
  var checkDrift = ({
1071
1211
  readers = READERS,
1072
1212
  root,
1073
- userSettings = join4(homedir(), SETTINGS),
1213
+ userSettings = join5(homedir(), SETTINGS),
1074
1214
  workspaces
1075
1215
  }) => {
1076
1216
  const config = readGeonosis(root);
@@ -1083,90 +1223,15 @@ var checkDrift = ({
1083
1223
  ...generatedFiles(root, workspaces),
1084
1224
  ...pluginDirs(root),
1085
1225
  ...publicHoists(root, workspaces),
1086
- ...gitHooks(root),
1226
+ ...composedHere(root, workspaces),
1227
+ ...lintScopes(root, workspaces),
1228
+ ...platformSplitBuilds(root),
1087
1229
  ...law(root, config),
1088
1230
  ...hooks(root, userSettings),
1089
1231
  ...blocks(root, config, readers, workspaces)
1090
1232
  ];
1091
1233
  };
1092
1234
 
1093
- // src/envelope.ts
1094
- import { existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync5 } from "fs";
1095
- import { join as join5 } from "path";
1096
- var ENVELOPES_DIR = ".geonosis/envelopes";
1097
- var NO_ENVELOPES = "no .geonosis/envelopes/*.json \u2014 the tools write one per run, so an absent envelope is a run nobody has made here yet, and it is not a balanced one";
1098
- var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1099
- var finding4 = (verdict, subject, message) => ({
1100
- check: "envelope",
1101
- message,
1102
- subject,
1103
- verdict
1104
- });
1105
- var lengthOf = (value) => Array.isArray(value) ? value.length : void 0;
1106
- var NEXT = "Next: re-run the tool that wrote it and read the numbers it prints \u2014 a run that has lost count of its own inputs is a bug in that tool, not in this tree";
1107
- var judge = (subject, name, parsed) => {
1108
- if (!isRecord2(parsed)) return finding4("FAIL", subject, `is not an object. ${NEXT}`);
1109
- const tool = parsed["tool"];
1110
- if (typeof tool !== "string" || tool.trim() === "") {
1111
- return finding4(
1112
- "FAIL",
1113
- subject,
1114
- `names no tool, so nothing here says which run it is about. ${NEXT}`
1115
- );
1116
- }
1117
- if (tool !== name) {
1118
- return finding4(
1119
- "FAIL",
1120
- subject,
1121
- `is written by "${tool}", not "${name}" \u2014 a tool writing another tool's envelope leaves both of their numbers unattributable. ${NEXT}`
1122
- );
1123
- }
1124
- const version = parsed["version"];
1125
- if (typeof version !== "string" || version.trim() === "") {
1126
- return finding4(
1127
- "FAIL",
1128
- subject,
1129
- `${tool}: names no version, so nothing dates this run and a stale envelope reads exactly like a fresh one. ${NEXT}`
1130
- );
1131
- }
1132
- const considered = parsed["considered"];
1133
- const read = parsed["read"];
1134
- const refused = lengthOf(parsed["refused"]);
1135
- const excused = lengthOf(parsed["excused"]);
1136
- if (typeof considered !== "number" || typeof read !== "number" || refused === void 0 || excused === void 0) {
1137
- return finding4(
1138
- "FAIL",
1139
- subject,
1140
- `${tool}: has no considered/read/refused/excused to check \u2014 the four numbers ARE the envelope, and a file without them measures nothing. ${NEXT}`
1141
- );
1142
- }
1143
- const accounted = read + refused + excused;
1144
- return considered === accounted ? finding4(
1145
- "OK",
1146
- subject,
1147
- `${tool} considered ${considered} and accounts for all of them \u2014 ${read} read + ${refused} refused + ${excused} excused`
1148
- ) : finding4(
1149
- "FAIL",
1150
- subject,
1151
- `${tool}: considered ${considered} but accounts for ${accounted} \u2014 ${read} read + ${refused} refused + ${excused} excused. It reported on fewer things than it was handed, and every verdict it printed is over the smaller number. ${NEXT}`
1152
- );
1153
- };
1154
- var checkEnvelopes = ({ root }) => {
1155
- const dir = join5(root, ENVELOPES_DIR);
1156
- const files = existsSync4(dir) ? readdirSync3(dir).filter((name) => name.endsWith(".json")).toSorted() : [];
1157
- if (files.length === 0) return [finding4("SKIP", ENVELOPES_DIR, NO_ENVELOPES)];
1158
- return files.map((name) => {
1159
- const subject = `${ENVELOPES_DIR}/${name}`;
1160
- let parsed;
1161
- try {
1162
- parsed = JSON.parse(readFileSync5(join5(dir, name), "utf8"));
1163
- } catch (error) {
1164
- return finding4("FAIL", subject, `is not readable JSON: ${error.message}. ${NEXT}`);
1165
- }
1166
- return judge(subject, name.replace(/\.json$/, ""), parsed);
1167
- });
1168
- };
1169
-
1170
1235
  // src/exercised.ts
1171
1236
  import { spawnSync as spawnSync2 } from "child_process";
1172
1237
  import {
@@ -1185,7 +1250,7 @@ var OFF = /* @__PURE__ */ new Set([0, "0", "allow", "off", false]);
1185
1250
  var severityOf = (level) => Array.isArray(level) ? level[0] : level;
1186
1251
  var enabledRulesOf = (rules, plugin) => Object.keys(rules).filter((id) => id.startsWith(`${plugin}/`) && !OFF.has(severityOf(rules[id]))).toSorted();
1187
1252
  var passes = (findings) => !findings.some((one) => one.verdict === "FAIL" || one.verdict === "UNJUDGED");
1188
- var finding5 = (subject, verdict, message) => ({
1253
+ var finding4 = (subject, verdict, message) => ({
1189
1254
  check: "exercised",
1190
1255
  message,
1191
1256
  subject,
@@ -1193,8 +1258,8 @@ var finding5 = (subject, verdict, message) => ({
1193
1258
  });
1194
1259
  var LIMIT = 800;
1195
1260
  var refusal = (error) => {
1196
- const said = String(error.message).trim();
1197
- return said.length > LIMIT ? `${said.slice(0, LIMIT)}\u2026` : said;
1261
+ const said2 = String(error.message).trim();
1262
+ return said2.length > LIMIT ? `${said2.slice(0, LIMIT)}\u2026` : said2;
1198
1263
  };
1199
1264
  var reasonFrom = (config, oxlint) => {
1200
1265
  const dir = mkdtempSync(join6(tmpdir(), "geonosis-doctor-why-"));
@@ -1206,8 +1271,8 @@ var reasonFrom = (config, oxlint) => {
1206
1271
  ["--no-ignore", "--disable-nested-config", "--config", config, probe],
1207
1272
  { encoding: "utf8", env: { ...process.env, NO_COLOR: "1" }, maxBuffer: 8 * 1024 * 1024 }
1208
1273
  );
1209
- const said = `${run.stdout ?? ""}${run.stderr ?? ""}`;
1210
- return /Error:\s*([^]+?)(?=\s+at\s+\S+\s+\(|\n|$)/.exec(said)?.[1];
1274
+ const said2 = `${run.stdout ?? ""}${run.stderr ?? ""}`;
1275
+ return /[A-Z]\w*:\s*([^]+?)(?=\s+at\s+\S+\s+\(|\n|$)/.exec(said2)?.[1];
1211
1276
  } catch {
1212
1277
  return void 0;
1213
1278
  } finally {
@@ -1366,6 +1431,7 @@ var throughProbes = ({
1366
1431
  rmSync(dir, { force: true, recursive: true });
1367
1432
  }
1368
1433
  };
1434
+ var BASE = "fired in the corpus and the probes, which says nothing about this tree \u2014 a rule can be exercised there and reach nothing a workspace here actually writes";
1369
1435
  var checkExercised = async ({
1370
1436
  config,
1371
1437
  corpus,
@@ -1374,15 +1440,15 @@ var checkExercised = async ({
1374
1440
  repoCorpus,
1375
1441
  root
1376
1442
  }) => {
1377
- const said = (verdict, message) => finding5(config.relative, verdict, message);
1443
+ const said2 = (verdict, message) => finding4(config.relative, verdict, message);
1378
1444
  if (repoCorpus !== void 0 && !existsSync5(repoCorpus)) {
1379
- return said(
1445
+ return said2(
1380
1446
  "FAIL",
1381
1447
  `geonosis.json declares a reach corpus at ${relativeToRoot(root, repoCorpus)} and there is nothing there \u2014 a corpus that cannot be read is a claim, not evidence`
1382
1448
  );
1383
1449
  }
1384
1450
  if (!existsSync5(corpus)) {
1385
- return said(
1451
+ return said2(
1386
1452
  "SKIP",
1387
1453
  `the plugin loaded from here ships no corpus at ${relativeToRoot(root, corpus)} \u2014 nothing declares which rules it can be evidence about`
1388
1454
  );
@@ -1391,15 +1457,15 @@ var checkExercised = async ({
1391
1457
  try {
1392
1458
  manifest = readManifest(corpus);
1393
1459
  } catch (error) {
1394
- return said("SKIP", refusal(error));
1460
+ return said2("SKIP", refusal(error));
1395
1461
  }
1396
1462
  const enabled = enabledHere(config, manifest.plugin);
1397
- if (enabled.length === 0) return said("SKIP", `no ${manifest.plugin} rule is enabled here`);
1463
+ if (enabled.length === 0) return said2("SKIP", `no ${manifest.plugin} rule is enabled here`);
1398
1464
  let reach;
1399
1465
  try {
1400
1466
  reach = corpusOf({ configA: config.path, configB: config.path, corpus, oxlint }).reach;
1401
1467
  } catch (error) {
1402
- return said(
1468
+ return said2(
1403
1469
  "FAIL",
1404
1470
  `oxlint refused to run this config over the corpus, so nothing here fired at all \u2014 ${reasonFrom(config.path, oxlint) ?? refusal(error)}`
1405
1471
  );
@@ -1407,7 +1473,7 @@ var checkExercised = async ({
1407
1473
  const fired = new Set(reach.filter((one) => one.firedInA).map((one) => one.rule));
1408
1474
  const unknown = enabled.filter((rule) => !manifest.rules.includes(rule));
1409
1475
  if (unknown.length > 0) {
1410
- return said(
1476
+ return said2(
1411
1477
  "FAIL",
1412
1478
  `${countOf(unknown, "name")} the loaded plugin does not export: ${named(unknown)}`
1413
1479
  );
@@ -1417,12 +1483,12 @@ var checkExercised = async ({
1417
1483
  try {
1418
1484
  own = ownReach({ config, oxlint, repoCorpus });
1419
1485
  } catch (error) {
1420
- return said("FAIL", `this repo's own corpus could not be run \u2014 ${refusal(error)}`);
1486
+ return said2("FAIL", `this repo's own corpus could not be run \u2014 ${refusal(error)}`);
1421
1487
  }
1422
1488
  }
1423
1489
  const empty = own.claimed.filter((rule) => !own.fired.has(rule));
1424
1490
  if (empty.length > 0) {
1425
- return said(
1491
+ return said2(
1426
1492
  "FAIL",
1427
1493
  `${countOf(empty, "fire")} nowhere in this repo's own corpus, which names them: ${named(empty)}`
1428
1494
  );
@@ -1430,7 +1496,7 @@ var checkExercised = async ({
1430
1496
  const silent = enabled.filter((rule) => !fired.has(rule) && !own.fired.has(rule));
1431
1497
  const where = own.claimed.length === 0 ? "" : ` (${own.fired.size} by this repo's own corpus)`;
1432
1498
  if (silent.length === 0) {
1433
- return said("OK", `${enabled.length} enabled, ${enabled.length} exercised${where}`);
1499
+ return said2("OK", `${enabled.length} enabled, ${enabled.length} exercised${where} \u2014 ${BASE}`);
1434
1500
  }
1435
1501
  const loadedProbes = entry === void 0 ? null : await probesOf(entry, manifest.plugin).catch(() => null);
1436
1502
  const probes = loadedProbes ?? {};
@@ -1441,14 +1507,14 @@ var checkExercised = async ({
1441
1507
  try {
1442
1508
  probed = throughProbes({ config, corpus, oxlint, probes, silent: declared });
1443
1509
  } catch (error) {
1444
- return said("FAIL", `the declared probes could not be run \u2014 ${refusal(error)}`);
1510
+ return said2("FAIL", `the declared probes could not be run \u2014 ${refusal(error)}`);
1445
1511
  }
1446
1512
  }
1447
1513
  const exercised = declared.filter((rule) => probed.fired.has(rule));
1448
1514
  const inert = declared.filter((rule) => !probed.fired.has(rule));
1449
1515
  const placed = inert.filter((rule) => !probed.refused.has(rule));
1450
1516
  if (placed.length > 0) {
1451
- return said(
1517
+ return said2(
1452
1518
  "FAIL",
1453
1519
  `${countOf(placed, "fire")} nowhere in the corpus and nothing through the probe each declares either: ${named(placed.map((rule) => under(config, rule)))}`
1454
1520
  );
@@ -1459,62 +1525,379 @@ var checkExercised = async ({
1459
1525
  (rule) => `${rule} declares no probe, so ${optionSetsOf(config, rule).every((options) => options.length === 0) ? "its scope" : under(config, rule)} does not reach the corpus`
1460
1526
  ) : [];
1461
1527
  if (unplaceable.length > 0 || unknowable.length > 0) {
1462
- return said(
1528
+ return said2(
1463
1529
  "UNJUDGED",
1464
1530
  named([...unknowable, ...unplaceable]) + (kitNote === "" ? "" : ` \u2014 and ${kitNote}`)
1465
1531
  );
1466
1532
  }
1467
- if (kitNote !== "") return said("WARN", kitNote);
1533
+ if (kitNote !== "") return said2("WARN", kitNote);
1468
1534
  const through = exercised.length === 1 ? "1 through its declared probe under this repo\u2019s options" : `${exercised.length} through their declared probes under this repo\u2019s options`;
1469
- return said("OK", `${enabled.length} enabled, ${enabled.length} exercised${where} \u2014 ${through}`);
1535
+ return said2(
1536
+ "OK",
1537
+ `${enabled.length} enabled, ${enabled.length} exercised${where} \u2014 ${through}. ${BASE}`
1538
+ );
1470
1539
  };
1471
1540
 
1472
- // src/loaded.ts
1473
- import { sep as sep3 } from "path";
1474
- var SCOPE = "@geonosis/";
1475
- var BLOCKS = [
1476
- "dependencies",
1477
- "devDependencies",
1478
- "optionalDependencies",
1479
- "peerDependencies"
1480
- ];
1481
- var RELEASE = /^v?(\d+)\.(\d+)\.(\d+)$/;
1482
- var PINNED = /^[=v]?(\d+\.\d+\.\d+(?:[-+][\w.-]+)?)$/;
1483
- var RANGE = /^(\^|~|>=)\s*v?(\d+)\.(\d+)\.(\d+)$/;
1484
- var LINKED = /^(?:file|link|portal|workspace):/;
1485
- var ANY = /* @__PURE__ */ new Set(["", "*", "latest", "x"]);
1486
- var versionOf = (found, at) => Number(found[at] ?? Number.NaN);
1487
- var parts = (found) => [
1488
- versionOf(found, 1),
1489
- versionOf(found, 2),
1490
- versionOf(found, 3)
1491
- ];
1492
- var below = (here, bound) => {
1493
- for (const [at, one] of here.entries()) {
1494
- const other = bound[at] ?? 0;
1495
- if (one !== other) return one < other;
1541
+ // src/envelope.ts
1542
+ import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync7 } from "fs";
1543
+ import { join as join7 } from "path";
1544
+ var ENVELOPES_DIR = ".geonosis/envelopes";
1545
+ var NO_ENVELOPES = "no .geonosis/envelopes/*.json \u2014 the tools write one per run, so an absent envelope is a run nobody has made here yet, and it is not a balanced one";
1546
+ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1547
+ var finding5 = (verdict, subject, message) => ({
1548
+ check: "envelope",
1549
+ message,
1550
+ subject,
1551
+ verdict
1552
+ });
1553
+ var lengthOf = (value) => Array.isArray(value) ? value.length : void 0;
1554
+ var NEXT = "Next: re-run the tool that wrote it and read the numbers it prints \u2014 a run that has lost count of its own inputs is a bug in that tool, not in this tree";
1555
+ var judge = (subject, name, parsed) => {
1556
+ if (!isRecord2(parsed)) return finding5("FAIL", subject, `is not an object. ${NEXT}`);
1557
+ const tool = parsed["tool"];
1558
+ if (typeof tool !== "string" || tool.trim() === "") {
1559
+ return finding5(
1560
+ "FAIL",
1561
+ subject,
1562
+ `names no tool, so nothing here says which run it is about. ${NEXT}`
1563
+ );
1496
1564
  }
1497
- return false;
1498
- };
1499
- var satisfies = (version, spec) => {
1500
- const wanted = spec.trim();
1501
- if (LINKED.test(wanted) || ANY.has(wanted)) return true;
1502
- const range = RANGE.exec(wanted);
1503
- if (range === null) {
1504
- const pinned = PINNED.exec(wanted);
1505
- return pinned === null ? void 0 : pinned[1] === version.replace(/^v/, "");
1565
+ if (tool !== name) {
1566
+ return finding5(
1567
+ "FAIL",
1568
+ subject,
1569
+ `is written by "${tool}", not "${name}" \u2014 a tool writing another tool's envelope leaves both of their numbers unattributable. ${NEXT}`
1570
+ );
1506
1571
  }
1507
- const here = RELEASE.exec(version);
1508
- if (here === null) return void 0;
1509
- const at = parts(here);
1510
- const bound = [
1511
- versionOf(range, 2),
1512
- versionOf(range, 3),
1513
- versionOf(range, 4)
1514
- ];
1515
- if (below(at, bound)) return false;
1516
- if (range[1] === ">=") return true;
1517
- if (range[1] === "~") return at[0] === bound[0] && at[1] === bound[1];
1572
+ const version = parsed["version"];
1573
+ if (typeof version !== "string" || version.trim() === "") {
1574
+ return finding5(
1575
+ "FAIL",
1576
+ subject,
1577
+ `${tool}: names no version, so nothing dates this run and a stale envelope reads exactly like a fresh one. ${NEXT}`
1578
+ );
1579
+ }
1580
+ const considered = parsed["considered"];
1581
+ const read = parsed["read"];
1582
+ const refused = lengthOf(parsed["refused"]);
1583
+ const excused = lengthOf(parsed["excused"]);
1584
+ if (typeof considered !== "number" || typeof read !== "number" || refused === void 0 || excused === void 0) {
1585
+ return finding5(
1586
+ "FAIL",
1587
+ subject,
1588
+ `${tool}: has no considered/read/refused/excused to check \u2014 the four numbers ARE the envelope, and a file without them measures nothing. ${NEXT}`
1589
+ );
1590
+ }
1591
+ const accounted = read + refused + excused;
1592
+ return considered === accounted ? finding5(
1593
+ "OK",
1594
+ subject,
1595
+ `${tool} considered ${considered} and accounts for all of them \u2014 ${read} read + ${refused} refused + ${excused} excused`
1596
+ ) : finding5(
1597
+ "FAIL",
1598
+ subject,
1599
+ `${tool}: considered ${considered} but accounts for ${accounted} \u2014 ${read} read + ${refused} refused + ${excused} excused. It reported on fewer things than it was handed, and every verdict it printed is over the smaller number. ${NEXT}`
1600
+ );
1601
+ };
1602
+ var checkEnvelopes = ({ root }) => {
1603
+ const dir = join7(root, ENVELOPES_DIR);
1604
+ const files = existsSync6(dir) ? readdirSync4(dir).filter((name) => name.endsWith(".json")).toSorted() : [];
1605
+ if (files.length === 0) return [finding5("SKIP", ENVELOPES_DIR, NO_ENVELOPES)];
1606
+ return files.map((name) => {
1607
+ const subject = `${ENVELOPES_DIR}/${name}`;
1608
+ let parsed;
1609
+ try {
1610
+ parsed = JSON.parse(readFileSync7(join7(dir, name), "utf8"));
1611
+ } catch (error) {
1612
+ return finding5("FAIL", subject, `is not readable JSON: ${error.message}. ${NEXT}`);
1613
+ }
1614
+ return judge(subject, name.replace(/\.json$/, ""), parsed);
1615
+ });
1616
+ };
1617
+
1618
+ // src/repo-corpus.ts
1619
+ import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
1620
+ import { join as join8 } from "path";
1621
+ var GEONOSIS_FILE = "geonosis.json";
1622
+ var repoCorpusOf = (root) => {
1623
+ const path = join8(root, GEONOSIS_FILE);
1624
+ if (!existsSync7(path)) return void 0;
1625
+ try {
1626
+ const parsed = JSON.parse(readFileSync8(path, "utf8"));
1627
+ const declared = parsed.doctor?.corpus;
1628
+ return typeof declared === "string" && declared !== "" ? join8(root, declared) : void 0;
1629
+ } catch {
1630
+ return void 0;
1631
+ }
1632
+ };
1633
+
1634
+ // src/group.ts
1635
+ import { existsSync as existsSync8, readFileSync as readFileSync9 } from "fs";
1636
+ import { join as join9 } from "path";
1637
+ var FIXED_GROUP = [
1638
+ "@geonosis/cli",
1639
+ "@geonosis/doctor",
1640
+ "@geonosis/evals",
1641
+ "@geonosis/integrations",
1642
+ "@geonosis/ledger",
1643
+ "@geonosis/lint-parity",
1644
+ "@geonosis/mcp",
1645
+ "@geonosis/observability",
1646
+ "@geonosis/oxlint-plugin-biological-architecture",
1647
+ "@geonosis/policy",
1648
+ "@geonosis/rails",
1649
+ "@geonosis/ratchet",
1650
+ "@geonosis/release",
1651
+ "@geonosis/review",
1652
+ "@geonosis/testbed",
1653
+ "@geonosis/themekit",
1654
+ "@geonosis/verify",
1655
+ "@geonosis/verify-arch",
1656
+ "@geonosis/visual-diff",
1657
+ "@geonosis/walk",
1658
+ "create-geonosis",
1659
+ "geonosis"
1660
+ ];
1661
+ var KIT_GROUP = { name: "@geonosis fixed group", packages: FIXED_GROUP };
1662
+ var declaredGroupsOf = (root) => {
1663
+ const path = join9(root, GEONOSIS_FILE);
1664
+ if (!existsSync8(path)) return void 0;
1665
+ let parsed;
1666
+ try {
1667
+ parsed = JSON.parse(readFileSync9(path, "utf8"));
1668
+ } catch {
1669
+ return void 0;
1670
+ }
1671
+ const declared = parsed.doctor?.groups;
1672
+ if (declared === void 0) return void 0;
1673
+ if (!Array.isArray(declared) || declared.length === 0) {
1674
+ throw new DoctorError(
1675
+ `${GEONOSIS_FILE} declares doctor.groups and names no group in it \u2014 say which packages must move together, or remove the key and get the kit's own group`
1676
+ );
1677
+ }
1678
+ return declared.map((one, at) => {
1679
+ const group = one;
1680
+ if (typeof group.name !== "string" || !Array.isArray(group.packages) || group.packages.length < 2) {
1681
+ throw new DoctorError(
1682
+ `${GEONOSIS_FILE} doctor.groups[${at}] needs a name and at least two packages \u2014 one package cannot disagree with itself`
1683
+ );
1684
+ }
1685
+ return { name: group.name, packages: group.packages };
1686
+ });
1687
+ };
1688
+ var labelOf = (workspace) => workspace.relative === "" ? "root" : workspace.relative;
1689
+ var versionOf = (dir, name) => {
1690
+ let entry;
1691
+ try {
1692
+ entry = resolveFrom(dir, name);
1693
+ } catch {
1694
+ return { kind: "absent" };
1695
+ }
1696
+ try {
1697
+ const manifest = JSON.parse(
1698
+ readFileSync9(join9(packageDirOf(entry, name), "package.json"), "utf8")
1699
+ );
1700
+ return typeof manifest.version === "string" ? { kind: "read", version: manifest.version } : { kind: "unreadable", why: "its package.json declares no version" };
1701
+ } catch (error) {
1702
+ return { kind: "unreadable", why: String(error.message).split("\n")[0] ?? "" };
1703
+ }
1704
+ };
1705
+ var finding6 = (subject, verdict, message) => ({
1706
+ check: "group",
1707
+ message,
1708
+ subject,
1709
+ verdict
1710
+ });
1711
+ var said = (versions) => [...versions.entries()].map(([version, from]) => `${version} (${from.join(", ")})`).join("; ");
1712
+ var repoWide = (name, found) => {
1713
+ const versions = /* @__PURE__ */ new Map();
1714
+ for (const one of found) versions.set(one.version, [...versions.get(one.version) ?? [], one.at]);
1715
+ if (versions.size === 1) return void 0;
1716
+ return finding6(
1717
+ name,
1718
+ "FAIL",
1719
+ `${versions.size} versions in one tree \u2014 ${said(versions)}. They are published as one number, so a tree holding two is a tree where one workspace is running code another has already replaced; remove the nested copies and reinstall`
1720
+ );
1721
+ };
1722
+ var perWorkspace = (group, at, found) => {
1723
+ if (new Set(found.map((one) => one.version)).size === 1) return void 0;
1724
+ const listed = found.map((one) => `${one.name} ${one.version}`).join(", ");
1725
+ return finding6(
1726
+ at,
1727
+ "FAIL",
1728
+ `${group.name} does not hold together here \u2014 ${listed}. Every member is cut on one version, so a workspace resolving two of them is a bump that landed halfway`
1729
+ );
1730
+ };
1731
+ var DOORS = ["@geonosis/cli", "geonosis"];
1732
+ var manifestOf = (dir, name) => {
1733
+ try {
1734
+ return JSON.parse(
1735
+ readFileSync9(join9(packageDirOf(resolveFrom(dir, name), name), "package.json"), "utf8")
1736
+ );
1737
+ } catch {
1738
+ return void 0;
1739
+ }
1740
+ };
1741
+ var binNamesOf = (manifest, name) => {
1742
+ const declared = manifest?.bin;
1743
+ return typeof declared === "string" ? [name.replace(/^@[^/]+\//, "")] : Object.keys(declared ?? {});
1744
+ };
1745
+ var declaredIn2 = (manifest) => [
1746
+ ...new Set(
1747
+ [
1748
+ manifest.dependencies,
1749
+ manifest.devDependencies,
1750
+ manifest.optionalDependencies,
1751
+ manifest.peerDependencies
1752
+ ].flatMap((block) => Object.keys(block ?? {}))
1753
+ )
1754
+ ];
1755
+ var rootDeclarations = (root, workspaces) => {
1756
+ const here = workspaces.find((one) => one.relative === "");
1757
+ if (here === void 0) return [];
1758
+ const declared = declaredIn2(here.manifest);
1759
+ const door = DOORS.find((name) => declared.includes(name));
1760
+ if (door === void 0) return [];
1761
+ const owned = new Set(
1762
+ [door, ...DOORS].flatMap((name) => {
1763
+ const manifest = manifestOf(root, name);
1764
+ return manifest === void 0 ? [] : Object.keys(manifest.dependencies ?? {});
1765
+ })
1766
+ );
1767
+ const callers = [
1768
+ ...Object.values(here.manifest.scripts ?? {}),
1769
+ ...hookLines(root).map((one) => one.line)
1770
+ ].join("\n");
1771
+ const spare = declared.filter(
1772
+ (name) => !DOORS.includes(name) && owned.has(name) && !binNamesOf(manifestOf(root, name), name).some(
1773
+ (bin) => new RegExp(`(?:^|[\\s;&|(/])${bin}(?:\\s|$)`).test(callers)
1774
+ )
1775
+ );
1776
+ if (spare.length === 0) {
1777
+ return [
1778
+ finding6(
1779
+ "package.json",
1780
+ "OK",
1781
+ `every @geonosis package this root declares is ${door} or a bin its own scripts call by name`
1782
+ )
1783
+ ];
1784
+ }
1785
+ return spare.map(
1786
+ (name) => finding6(
1787
+ name,
1788
+ "WARN",
1789
+ `the root declares it and no root script calls its bin \u2014 ${door} already brings it, so this second declaration is a version that can disagree with the door's, and every tool that asks what is used here reads it as unused. Drop it from the root, or declare it in the workspace whose code imports it`
1790
+ )
1791
+ );
1792
+ };
1793
+ var checkGroup = ({
1794
+ groups,
1795
+ root,
1796
+ workspaces
1797
+ }) => {
1798
+ const findings = [...rootDeclarations(root, workspaces)];
1799
+ for (const group of groups ?? [KIT_GROUP]) {
1800
+ const found = [];
1801
+ for (const workspace of workspaces) {
1802
+ const at = labelOf(workspace);
1803
+ const here = [];
1804
+ for (const name of group.packages) {
1805
+ const reading = versionOf(workspace.dir, name);
1806
+ if (reading.kind === "read") here.push({ at, name, version: reading.version });
1807
+ if (reading.kind === "unreadable") {
1808
+ findings.push(
1809
+ finding6(
1810
+ `${at} \u2192 ${name}`,
1811
+ "UNJUDGED",
1812
+ `it resolves from here and ${reading.why} \u2014 this copy could be any version, so the group cannot be said to hold or not to hold`
1813
+ )
1814
+ );
1815
+ }
1816
+ }
1817
+ const split2 = here.length > 0 ? perWorkspace(group, at, here) : void 0;
1818
+ if (split2 !== void 0) findings.push(split2);
1819
+ found.push(...here);
1820
+ }
1821
+ if (found.length === 0) {
1822
+ findings.push(
1823
+ finding6(
1824
+ group.name,
1825
+ "SKIP",
1826
+ `no workspace under ${root} resolves any of ${group.packages.join(", ")}, so nothing here versions together`
1827
+ )
1828
+ );
1829
+ continue;
1830
+ }
1831
+ const names = [...new Set(found.map((one) => one.name))].toSorted();
1832
+ const split = names.flatMap((name) => {
1833
+ const line = repoWide(
1834
+ name,
1835
+ found.filter((one) => one.name === name)
1836
+ );
1837
+ return line === void 0 ? [] : [line];
1838
+ });
1839
+ findings.push(...split);
1840
+ if (split.length === 0) {
1841
+ const versions = [...new Set(found.map((one) => one.version))];
1842
+ findings.push(
1843
+ finding6(
1844
+ group.name,
1845
+ "OK",
1846
+ `${names.length} of ${group.packages.length} installed, at ${versions.join(", ")}, one copy each across ${new Set(found.map((one) => one.at)).size} workspaces`
1847
+ )
1848
+ );
1849
+ }
1850
+ }
1851
+ return findings;
1852
+ };
1853
+
1854
+ // src/loaded.ts
1855
+ import { readFileSync as readFileSync10 } from "fs";
1856
+ import { join as join10, sep as sep3 } from "path";
1857
+ var SCOPE = "@geonosis/";
1858
+ var BLOCKS = [
1859
+ "dependencies",
1860
+ "devDependencies",
1861
+ "optionalDependencies",
1862
+ "peerDependencies"
1863
+ ];
1864
+ var RELEASE = /^v?(\d+)\.(\d+)\.(\d+)$/;
1865
+ var PINNED = /^[=v]?(\d+\.\d+\.\d+(?:[-+][\w.-]+)?)$/;
1866
+ var RANGE = /^(\^|~|>=)\s*v?(\d+)\.(\d+)\.(\d+)$/;
1867
+ var LINKED = /^(?:file|link|portal|workspace):/;
1868
+ var ANY = /* @__PURE__ */ new Set(["", "*", "latest", "x"]);
1869
+ var versionOf2 = (found, at) => Number(found[at] ?? Number.NaN);
1870
+ var parts = (found) => [
1871
+ versionOf2(found, 1),
1872
+ versionOf2(found, 2),
1873
+ versionOf2(found, 3)
1874
+ ];
1875
+ var below = (here, bound) => {
1876
+ for (const [at, one] of here.entries()) {
1877
+ const other = bound[at] ?? 0;
1878
+ if (one !== other) return one < other;
1879
+ }
1880
+ return false;
1881
+ };
1882
+ var satisfies = (version, spec) => {
1883
+ const wanted = spec.trim();
1884
+ if (LINKED.test(wanted) || ANY.has(wanted)) return true;
1885
+ const range = RANGE.exec(wanted);
1886
+ if (range === null) {
1887
+ const pinned = PINNED.exec(wanted);
1888
+ return pinned === null ? void 0 : pinned[1] === version.replace(/^v/, "");
1889
+ }
1890
+ const here = RELEASE.exec(version);
1891
+ if (here === null) return void 0;
1892
+ const at = parts(here);
1893
+ const bound = [
1894
+ versionOf2(range, 2),
1895
+ versionOf2(range, 3),
1896
+ versionOf2(range, 4)
1897
+ ];
1898
+ if (below(at, bound)) return false;
1899
+ if (range[1] === ">=") return true;
1900
+ if (range[1] === "~") return at[0] === bound[0] && at[1] === bound[1];
1518
1901
  if (bound[0] > 0) return at[0] === bound[0];
1519
1902
  if (bound[1] > 0) return at[0] === 0 && at[1] === bound[1];
1520
1903
  return at[0] === 0 && at[1] === 0 && at[2] === bound[2];
@@ -1540,7 +1923,7 @@ var declaredFor = ({
1540
1923
  }
1541
1924
  return void 0;
1542
1925
  };
1543
- var finding6 = (subject, verdict, message) => ({
1926
+ var finding7 = (subject, verdict, message) => ({
1544
1927
  check: "loaded",
1545
1928
  message,
1546
1929
  subject,
@@ -1557,36 +1940,36 @@ var oneConfig = async ({
1557
1940
  specifier,
1558
1941
  workspaces
1559
1942
  }) => {
1560
- const said = (verdict, message) => finding6(config.relative, verdict, `${specifier}: ${message}`);
1943
+ const said2 = (verdict, message) => finding7(config.relative, verdict, `${specifier}: ${message}`);
1561
1944
  let loaded;
1562
1945
  try {
1563
1946
  loaded = await versionAt(resolveFrom(config.dir, specifier), specifier, root);
1564
1947
  } catch (error) {
1565
- return said(
1948
+ return said2(
1566
1949
  "FAIL",
1567
1950
  `could not resolve it from this config's directory, and oxlint resolves it from exactly there \u2014 ${firstLine(error)}`
1568
1951
  );
1569
1952
  }
1570
1953
  const declared = declaredFor({ dir: config.dir, specifier, workspaces });
1571
1954
  if (declared === void 0) {
1572
- return said(
1955
+ return said2(
1573
1956
  "FAIL",
1574
1957
  `loaded ${loaded.version} from ${loaded.at}, and no package.json from here up to the root declares it \u2014 nothing says which version this was meant to be`
1575
1958
  );
1576
1959
  }
1577
1960
  const held = satisfies(loaded.version, declared.spec);
1578
1961
  if (held === void 0) {
1579
- return said(
1962
+ return said2(
1580
1963
  "FAIL",
1581
1964
  `loaded ${loaded.version}, and ${declared.at} declares "${declared.spec}" \u2014 a range this check cannot read, so it will not call the version right`
1582
1965
  );
1583
1966
  }
1584
- return held ? said("OK", `loaded ${loaded.version} = declared ${declared.spec} (${declared.at})`) : said(
1967
+ return held ? said2("OK", `loaded ${loaded.version} = declared ${declared.spec} (${declared.at})`) : said2(
1585
1968
  "FAIL",
1586
1969
  `loaded ${loaded.version}, declared ${declared.spec} (${declared.at}) \u2014 a nested copy at ${loaded.at}. Remove the nested copies (rm -rf <workspace>/node_modules/@geonosis) and reinstall; the linter is not running what the tree declares until then`
1587
1970
  );
1588
1971
  };
1589
- var labelOf = (workspace) => workspace.relative === "" ? "root" : workspace.relative;
1972
+ var labelOf2 = (workspace) => workspace.relative === "" ? "root" : workspace.relative;
1590
1973
  var copiesOf = async ({
1591
1974
  root,
1592
1975
  specifier,
@@ -1603,31 +1986,81 @@ var copiesOf = async ({
1603
1986
  const at = relativeToRoot(root, packageDirOf(entry, specifier));
1604
1987
  const already = found.get(at);
1605
1988
  if (already !== void 0) {
1606
- already.from.push(labelOf(workspace));
1989
+ already.from.push(labelOf2(workspace));
1607
1990
  continue;
1608
1991
  }
1609
- found.set(at, { from: [labelOf(workspace)], version: await pluginVersionOf(entry) });
1992
+ found.set(at, { from: [labelOf2(workspace)], version: await pluginVersionOf(entry) });
1610
1993
  }
1611
1994
  if (found.size === 0) {
1612
- return finding6(specifier, "FAIL", "no workspace in this tree can resolve it at all");
1995
+ return finding7(specifier, "FAIL", "no workspace in this tree can resolve it at all");
1613
1996
  }
1614
1997
  const listed = [...found.entries()].map(([at, one]) => `${at} ${one.version} (${one.from.join(", ")})`).join("; ");
1615
- return found.size === 1 ? finding6(specifier, "OK", `1 copy \u2014 ${listed}`) : finding6(
1998
+ return found.size === 1 ? finding7(specifier, "OK", `1 copy \u2014 ${listed}`) : finding7(
1616
1999
  specifier,
1617
2000
  "WARN",
1618
2001
  `${found.size} copies \u2014 ${listed}. Which one oxlint runs depends on which directory its config sits in.`
1619
2002
  );
1620
2003
  };
2004
+ var shipperOf = (root, bin) => {
2005
+ const name = bin === "geonosis" ? "@geonosis/cli" : `@geonosis/${bin.replace(/^geonosis-/, "")}`;
2006
+ try {
2007
+ const manifest = JSON.parse(
2008
+ readFileSync10(join10(packageDirOf(resolveFrom(root, name), name), "package.json"), "utf8")
2009
+ );
2010
+ const declared = manifest.bin;
2011
+ return typeof declared === "object" && !Object.hasOwn(declared, bin) ? "" : name;
2012
+ } catch {
2013
+ return name;
2014
+ }
2015
+ };
2016
+ var knipNote = (root, bin) => {
2017
+ const shipper = shipperOf(root, bin);
2018
+ return shipper === "" ? "" : `. knip resolves no dependency out of that path, so add ignoreDependencies: ["${shipper}"] to its config in the same commit`;
2019
+ };
2020
+ var gitHooks = (root) => {
2021
+ const lines = hookLines(root);
2022
+ if (lines.length === 0) {
2023
+ return [finding7("git hooks", "SKIP", "no lefthook, husky or .githooks file here to read")];
2024
+ }
2025
+ const files = [...new Set(lines.map((one) => one.at))].toSorted();
2026
+ return files.flatMap((at) => {
2027
+ const here = lines.filter((one) => one.at === at);
2028
+ const named2 = here.filter((one) => NAMES_A_BIN.test(one.line));
2029
+ if (named2.length === 0) {
2030
+ return [finding7(at, "SKIP", "it names no geonosis bin, so there is nothing here to start")];
2031
+ }
2032
+ const wrong = named2.flatMap((one) => {
2033
+ const found = VERSION_MANAGED.exec(one.line)?.groups;
2034
+ return found === void 0 ? [] : [{ bin: found["bin"] ?? "", runner: found["runner"] ?? "" }];
2035
+ });
2036
+ if (wrong.length === 0) {
2037
+ return [
2038
+ finding7(
2039
+ at,
2040
+ "OK",
2041
+ `${named2.length} geonosis bin(s) here, every one called directly rather than through a runner`
2042
+ )
2043
+ ];
2044
+ }
2045
+ return wrong.map(
2046
+ ({ bin, runner }) => finding7(
2047
+ at,
2048
+ "FAIL",
2049
+ `it runs ${bin} through ${runner} \u2014 a git hook's PATH is not the shell's and carries no version-manager shim, so ${runner} fails to START and what the author reads is its message, not this gate's. Call it directly: node_modules/.bin/${bin}${knipNote(root, bin)}`
2050
+ )
2051
+ );
2052
+ });
2053
+ };
1621
2054
  var checkLoaded = async ({
1622
2055
  configs,
1623
2056
  root,
1624
2057
  workspaces
1625
2058
  }) => {
1626
- const findings = [];
2059
+ const findings = [...gitHooks(root)];
1627
2060
  const specifiers = /* @__PURE__ */ new Set();
1628
2061
  for (const config of configs) {
1629
2062
  if (config.error !== void 0) {
1630
- findings.push(finding6(config.relative, "FAIL", config.error));
2063
+ findings.push(finding7(config.relative, "FAIL", config.error));
1631
2064
  continue;
1632
2065
  }
1633
2066
  for (const specifier of config.jsPlugins.filter((name) => name.startsWith(SCOPE))) {
@@ -1642,13 +2075,13 @@ var checkLoaded = async ({
1642
2075
  };
1643
2076
 
1644
2077
  // src/observability.ts
1645
- import { readFileSync as readFileSync7 } from "fs";
1646
- import { join as join7 } from "path";
1647
- var GEONOSIS_FILE = "geonosis.json";
2078
+ import { readFileSync as readFileSync11 } from "fs";
2079
+ import { join as join11 } from "path";
2080
+ var GEONOSIS_FILE2 = "geonosis.json";
1648
2081
  var REACHES_NOTHING = /* @__PURE__ */ new Set(["console", "memory", "noop", "none", "null", "swallowing"]);
1649
2082
  var DEFAULT_MAX_AGE_SECONDS = 3600;
1650
2083
  var HEAD_TIMEOUT_MS = 3e3;
1651
- var finding7 = (verdict, subject, message) => ({
2084
+ var finding8 = (verdict, subject, message) => ({
1652
2085
  check: "observability",
1653
2086
  message,
1654
2087
  subject,
@@ -1657,7 +2090,7 @@ var finding7 = (verdict, subject, message) => ({
1657
2090
  var readGeonosis2 = (root) => {
1658
2091
  let text;
1659
2092
  try {
1660
- text = readFileSync7(join7(root, GEONOSIS_FILE), "utf8");
2093
+ text = readFileSync11(join11(root, GEONOSIS_FILE2), "utf8");
1661
2094
  } catch {
1662
2095
  return { present: false };
1663
2096
  }
@@ -1675,27 +2108,27 @@ var readGeonosis2 = (root) => {
1675
2108
  var exporterFinding = (config) => {
1676
2109
  const sink = config.sink;
1677
2110
  if (typeof sink !== "string" || sink.trim() === "") {
1678
- return finding7(
2111
+ return finding8(
1679
2112
  "FAIL",
1680
- GEONOSIS_FILE,
2113
+ GEONOSIS_FILE2,
1681
2114
  "observability.sink is not set, so nothing here says where errors are supposed to go \u2014 and a repo that cannot name its exporter has not got one"
1682
2115
  );
1683
2116
  }
1684
2117
  if (REACHES_NOTHING.has(sink.toLowerCase())) {
1685
- return finding7(
2118
+ return finding8(
1686
2119
  "WARN",
1687
- GEONOSIS_FILE,
2120
+ GEONOSIS_FILE2,
1688
2121
  `the configured sink is "${sink}", which answers ok and reaches nothing. Correct in a dev tree; in a deployed one it is the instrument that cannot fail.`
1689
2122
  );
1690
2123
  }
1691
- return finding7("OK", GEONOSIS_FILE, `the configured sink is "${sink}"`);
2124
+ return finding8("OK", GEONOSIS_FILE2, `the configured sink is "${sink}"`);
1692
2125
  };
1693
2126
  var reachableFinding = async (config) => {
1694
2127
  const endpoint = config.endpoint;
1695
2128
  if (typeof endpoint !== "string" || endpoint.trim() === "") {
1696
- return finding7(
2129
+ return finding8(
1697
2130
  "SKIP",
1698
- GEONOSIS_FILE,
2131
+ GEONOSIS_FILE2,
1699
2132
  "no observability.endpoint was named, so whether the exporter is reachable was not asked"
1700
2133
  );
1701
2134
  }
@@ -1703,15 +2136,15 @@ var reachableFinding = async (config) => {
1703
2136
  const timer = setTimeout(() => controller.abort(), HEAD_TIMEOUT_MS);
1704
2137
  try {
1705
2138
  const response = await fetch(endpoint, { method: "HEAD", signal: controller.signal });
1706
- return finding7(
2139
+ return finding8(
1707
2140
  "OK",
1708
- GEONOSIS_FILE,
2141
+ GEONOSIS_FILE2,
1709
2142
  `${endpoint} is reachable \u2014 it answered ${response.status} to a HEAD`
1710
2143
  );
1711
2144
  } catch (error) {
1712
- return finding7(
2145
+ return finding8(
1713
2146
  "FAIL",
1714
- GEONOSIS_FILE,
2147
+ GEONOSIS_FILE2,
1715
2148
  `${endpoint} is not reachable from here: ${error.message}. Every report this repo sends is going into that.`
1716
2149
  );
1717
2150
  } finally {
@@ -1721,36 +2154,36 @@ var reachableFinding = async (config) => {
1721
2154
  var ageFinding = (config, root, now) => {
1722
2155
  const file = config.lastEventFile;
1723
2156
  if (typeof file !== "string" || file.trim() === "") {
1724
- return finding7(
2157
+ return finding8(
1725
2158
  "SKIP",
1726
- GEONOSIS_FILE,
2159
+ GEONOSIS_FILE2,
1727
2160
  "no observability.lastEventFile was configured, so when the last event arrived is not a question anything here can answer. Have the sink write { at, id, sink } on every capture and name the file."
1728
2161
  );
1729
2162
  }
1730
2163
  const maxAgeSeconds = typeof config.maxAgeSeconds === "number" && config.maxAgeSeconds > 0 ? config.maxAgeSeconds : DEFAULT_MAX_AGE_SECONDS;
1731
2164
  let record;
1732
2165
  try {
1733
- record = JSON.parse(readFileSync7(join7(root, file), "utf8"));
2166
+ record = JSON.parse(readFileSync11(join11(root, file), "utf8"));
1734
2167
  } catch (error) {
1735
- return finding7(
2168
+ return finding8(
1736
2169
  "FAIL",
1737
2170
  file,
1738
2171
  `the last event file could not be read: ${error.message}. A sink that has never written one has never captured anything.`
1739
2172
  );
1740
2173
  }
1741
2174
  if (typeof record.at !== "number" || !Number.isFinite(record.at)) {
1742
- return finding7(
2175
+ return finding8(
1743
2176
  "FAIL",
1744
2177
  file,
1745
2178
  'the last event record has no numeric "at", so its age cannot be read \u2014 and an age nobody can read is not an age inside the window'
1746
2179
  );
1747
2180
  }
1748
2181
  const ageSeconds = Math.round((now - record.at) / 1e3);
1749
- return ageSeconds > maxAgeSeconds ? finding7(
2182
+ return ageSeconds > maxAgeSeconds ? finding8(
1750
2183
  "FAIL",
1751
2184
  file,
1752
2185
  `the last event arrived ${ageSeconds}s ago, past the ${maxAgeSeconds}s window. An exporter that stopped, a key that was rotated and a sink that has been dropping since Tuesday all look exactly like this, and all of them leave a green build.`
1753
- ) : finding7(
2186
+ ) : finding8(
1754
2187
  "OK",
1755
2188
  file,
1756
2189
  `the last event arrived ${ageSeconds}s ago, inside the ${maxAgeSeconds}s window`
@@ -1759,18 +2192,18 @@ var ageFinding = (config, root, now) => {
1759
2192
  var probeFinding = (config) => {
1760
2193
  const probe = config.probe;
1761
2194
  if (typeof probe === "string" && probe.trim() !== "") {
1762
- return finding7("OK", GEONOSIS_FILE, `the probe that proves this exporter is "${probe}"`);
2195
+ return finding8("OK", GEONOSIS_FILE2, `the probe that proves this exporter is "${probe}"`);
1763
2196
  }
1764
2197
  if (typeof config.lastEventFile === "string" && config.lastEventFile.trim() !== "") {
1765
- return finding7(
2198
+ return finding8(
1766
2199
  "OK",
1767
- GEONOSIS_FILE,
2200
+ GEONOSIS_FILE2,
1768
2201
  "no probe command, but a last event file is read above, so something does look at this exporter"
1769
2202
  );
1770
2203
  }
1771
- return finding7(
2204
+ return finding8(
1772
2205
  "WARN",
1773
- GEONOSIS_FILE,
2206
+ GEONOSIS_FILE2,
1774
2207
  "neither observability.probe nor observability.lastEventFile is configured, so nothing in this repo has ever established that a report reaches the sink. Name a probe command \u2014 the doctor reports it, your gate runs it."
1775
2208
  );
1776
2209
  };
@@ -1781,19 +2214,19 @@ var checkObservability = async ({
1781
2214
  const read = readGeonosis2(root);
1782
2215
  if (read.error !== void 0) {
1783
2216
  return [
1784
- finding7(
2217
+ finding8(
1785
2218
  "FAIL",
1786
- GEONOSIS_FILE,
1787
- `${GEONOSIS_FILE} could not be parsed: ${read.error}. A config nobody can read has not been read, and every question below would have been answered from a default nobody chose.`
2219
+ GEONOSIS_FILE2,
2220
+ `${GEONOSIS_FILE2} could not be parsed: ${read.error}. A config nobody can read has not been read, and every question below would have been answered from a default nobody chose.`
1788
2221
  )
1789
2222
  ];
1790
2223
  }
1791
2224
  if (!read.present || read.config === void 0) {
1792
2225
  return [
1793
- finding7(
2226
+ finding8(
1794
2227
  "SKIP",
1795
- GEONOSIS_FILE,
1796
- `no observability block in ${GEONOSIS_FILE}, so nothing here knows where this repo sends its errors. Add { sink, endpoint, lastEventFile | probe, maxAgeSeconds } to have this asked.`
2228
+ GEONOSIS_FILE2,
2229
+ `no observability block in ${GEONOSIS_FILE2}, so nothing here knows where this repo sends its errors. Add { sink, endpoint, lastEventFile | probe, maxAgeSeconds } to have this asked.`
1797
2230
  )
1798
2231
  ];
1799
2232
  }
@@ -1806,28 +2239,14 @@ var checkObservability = async ({
1806
2239
  ];
1807
2240
  };
1808
2241
 
1809
- // src/repo-corpus.ts
1810
- import { existsSync as existsSync6, readFileSync as readFileSync8 } from "fs";
1811
- import { join as join8 } from "path";
1812
- var GEONOSIS_FILE2 = "geonosis.json";
1813
- var repoCorpusOf = (root) => {
1814
- const path = join8(root, GEONOSIS_FILE2);
1815
- if (!existsSync6(path)) return void 0;
1816
- try {
1817
- const parsed = JSON.parse(readFileSync8(path, "utf8"));
1818
- const declared = parsed.doctor?.corpus;
1819
- return typeof declared === "string" && declared !== "" ? join8(root, declared) : void 0;
1820
- } catch {
1821
- return void 0;
1822
- }
1823
- };
1824
-
1825
2242
  // src/runner.ts
2243
+ import { existsSync as existsSync9, readdirSync as readdirSync5, readFileSync as readFileSync12 } from "fs";
2244
+ import { join as join12 } from "path";
1826
2245
  var TEST_FAILURES = "testFailures";
1827
2246
  var RUNS_A_RUNNER = /(?:^|[\s;&|(])(?:npx\s+|bunx\s+|pnpm\s+(?:exec\s+)?)?(?:vitest|bun\s+test)(?:\s|$)/;
1828
2247
  var RUNS_BUN_TEST = /(?:^|[\s;&|(])(?:bunx\s+)?bun\s+test(?:\s|$)/;
1829
2248
  var WRITES_A_REPORT = /--reporter[= ]\S*json|--outputFile/i;
1830
- var finding8 = (subject, verdict, message) => ({
2249
+ var finding9 = (subject, verdict, message) => ({
1831
2250
  check: "runner",
1832
2251
  message,
1833
2252
  subject,
@@ -1845,57 +2264,334 @@ var reportingCounters = (ratchet) => (ratchet?.counters ?? []).filter(
1845
2264
  (entry) => entry.counter === TEST_FAILURES && stringOf(entry.report) !== ""
1846
2265
  );
1847
2266
  var keyOf = (entry) => stringOf(entry.key) === "" ? TEST_FAILURES : stringOf(entry.key);
2267
+ var PROVES = /--prove\b/;
2268
+ var everythingRun2 = (root, workspaces) => {
2269
+ const scripts = workspaces.flatMap((one) => Object.values(one.manifest.scripts ?? {}));
2270
+ const dir = join12(root, ".github/workflows");
2271
+ const workflows = (existsSync9(dir) ? readdirSync5(dir) : []).filter((name) => name.endsWith(".yml") || name.endsWith(".yaml")).map((name) => {
2272
+ try {
2273
+ return readFileSync12(join12(dir, name), "utf8");
2274
+ } catch {
2275
+ return "";
2276
+ }
2277
+ });
2278
+ let tiers = {};
2279
+ try {
2280
+ const config = JSON.parse(readFileSync12(join12(root, "geonosis.json"), "utf8"));
2281
+ tiers = config.verify ?? {};
2282
+ } catch {
2283
+ }
2284
+ return {
2285
+ run: [...Object.values(tiers).flat(), ...scripts, ...workflows].join("\n"),
2286
+ tiers: Object.keys(tiers)
2287
+ };
2288
+ };
2289
+ var proveIsWired = ({
2290
+ ratchet,
2291
+ root,
2292
+ workspaces
2293
+ }) => {
2294
+ if (ratchet === void 0 || ratchet.counters.length === 0) return [];
2295
+ const { run, tiers } = everythingRun2(root, workspaces);
2296
+ if (PROVES.test(run)) {
2297
+ return [
2298
+ finding9(
2299
+ RATCHET_FILE,
2300
+ "OK",
2301
+ `${ratchet.counters.length} counter(s), and something here runs geonosis-ratchet --prove`
2302
+ )
2303
+ ];
2304
+ }
2305
+ const where = tiers.at(-1) ?? "full";
2306
+ return [
2307
+ finding9(
2308
+ RATCHET_FILE,
2309
+ "WARN",
2310
+ `${ratchet.counters.length} counter(s) and nothing here ever runs geonosis-ratchet --prove \u2014 not a verify tier, not a script, not a workflow. A counter that has never been seen reading its own planted finding has not been shown to measure anything, and it reads exactly like a clean tree. Add \`geonosis-ratchet --prove\` to the "${where}" tier in geonosis.json, or to the workflow that runs it`
2311
+ )
2312
+ ];
2313
+ };
1848
2314
  var checkRunner = ({
1849
2315
  ratchet,
2316
+ root,
1850
2317
  workspaces
1851
2318
  }) => {
1852
2319
  const reading = reportingCounters(ratchet);
1853
- return workspaces.flatMap((workspace) => {
2320
+ const perWorkspace2 = workspaces.flatMap((workspace) => {
1854
2321
  const script = stringOf(workspace.manifest.scripts?.test);
1855
2322
  if (script === "") return [];
1856
2323
  const subject = workspace.relative === "" ? "package.json" : `${workspace.relative}/package.json`;
1857
- const said = (verdict, message) => [
1858
- finding8(subject, verdict, message)
2324
+ const said2 = (verdict, message) => [
2325
+ finding9(subject, verdict, message)
1859
2326
  ];
1860
2327
  if (!RUNS_A_RUNNER.test(script)) {
1861
- return said(
2328
+ return said2(
1862
2329
  "OK",
1863
2330
  `"${script}" runs neither vitest nor bun test \u2014 this check has nothing to say about it`
1864
2331
  );
1865
2332
  }
1866
2333
  if (testFilesUnder(workspace.dir).length === 0) {
1867
- return said(
2334
+ return said2(
1868
2335
  "SKIP",
1869
2336
  `"${script}" runs a test runner and there is no test file under this workspace \u2014 nothing here for its exit code to be wrong about`
1870
2337
  );
1871
2338
  }
1872
2339
  if (WRITES_A_REPORT.test(script)) {
1873
- return said("OK", "the script asks the runner for its own JSON report, not for a status code");
2340
+ return said2("OK", "the script asks the runner for its own JSON report, not for a status code");
1874
2341
  }
1875
2342
  const counter = reading.find((entry) => covers(stringOf(entry.command), workspace, workspaces));
1876
2343
  if (counter !== void 0) {
1877
- return said("OK", `read by the ratchet's "${keyOf(counter)}" counter in report mode`);
2344
+ return said2("OK", `read by the ratchet's "${keyOf(counter)}" counter in report mode`);
1878
2345
  }
1879
2346
  if (RUNS_BUN_TEST.test(script)) {
1880
- return said(
2347
+ return said2(
1881
2348
  "OK",
1882
2349
  `"${script}" is judged by its exit code, and bun test's exit code is a verdict: a planted failure exits 1, measured on bun 1.4.0 (2026-08-30). Nothing further is required here.`
1883
2350
  );
1884
2351
  }
1885
- return said(
2352
+ return said2(
1886
2353
  "WARN",
1887
2354
  `"${script}" runs vitest, whose exit code is the only verdict here; @cloudflare/vitest-pool-workers exited 0 on failing tests for weeks in a consumer. Give the ratchet a ${TEST_FAILURES} counter in report mode covering this workspace, or have the script write a JSON report.`
1888
2355
  );
1889
2356
  });
2357
+ return [...proveIsWired({ ratchet, root, workspaces }), ...perWorkspace2];
1890
2358
  };
1891
2359
 
1892
2360
  // src/doctor.ts
2361
+ import { homedir as homedir2 } from "os";
2362
+ import { join as join14 } from "path";
1893
2363
  import { resolveOxlint } from "@geonosis/lint-parity";
2364
+
2365
+ // src/engine.ts
2366
+ import { sep as sep4 } from "path";
2367
+ var declaredIn3 = (manifest) => new Set(
2368
+ [
2369
+ manifest.dependencies,
2370
+ manifest.devDependencies,
2371
+ manifest.optionalDependencies,
2372
+ manifest.peerDependencies
2373
+ ].flatMap((block) => Object.keys(block ?? {}))
2374
+ );
2375
+ var holds3 = (parent, child) => child === parent || child.startsWith(`${parent}${sep4}`);
2376
+ var reachableFrom = (dir, workspaces) => new Set(
2377
+ workspaces.filter((one) => holds3(one.dir, dir)).flatMap((one) => [...declaredIn3(one.manifest)])
2378
+ );
2379
+ var enabledHere2 = (config, plugin) => [
2380
+ .../* @__PURE__ */ new Set([
2381
+ ...enabledRulesOf(config.rules, plugin),
2382
+ ...config.overrides.flatMap((one) => enabledRulesOf(one.rules, plugin))
2383
+ ])
2384
+ ];
2385
+ var checkEngines = async ({
2386
+ config,
2387
+ entry,
2388
+ workspaces
2389
+ }) => {
2390
+ const read = await presumptionsOf(entry).catch(() => void 0);
2391
+ if (read === void 0 || read.namespace === "" || Object.keys(read.presumed).length === 0) {
2392
+ return [];
2393
+ }
2394
+ const declared = reachableFrom(config.dir, workspaces);
2395
+ return enabledHere2(config, read.namespace).flatMap((rule) => {
2396
+ const presumes = read.presumed[rule.slice(read.namespace.length + 1)];
2397
+ if (presumes === void 0 || presumes.packages.some((name) => declared.has(name))) return [];
2398
+ return [
2399
+ {
2400
+ check: "exercised",
2401
+ message: `it presumes ${presumes.engine}, and no manifest from this config up to the root declares ${presumes.packages.join(" or ")} \u2014 enabled here it has no possible finding, which reads exactly like a clean tree. Drop it from this config, or say which package brings the engine`,
2402
+ subject: `${config.relative} \u2192 ${rule}`,
2403
+ verdict: "WARN"
2404
+ }
2405
+ ];
2406
+ });
2407
+ };
2408
+
2409
+ // src/rails.ts
2410
+ import { existsSync as existsSync10, readFileSync as readFileSync13 } from "fs";
2411
+ import { join as join13 } from "path";
2412
+ var GEONOSIS2 = "geonosis.json";
2413
+ var PROJECT = ".claude/settings.json";
2414
+ var LOCAL = ".claude/settings.local.json";
2415
+ var RUN_RECORD = ".geonosis/rails-run.json";
2416
+ var managedSettingsPath = (platform = process.platform) => platform === "darwin" ? "/Library/Application Support/ClaudeCode/managed-settings.json" : "/etc/claude-code/managed-settings.json";
2417
+ var finding10 = (subject, verdict, message) => ({
2418
+ check: "rails",
2419
+ message,
2420
+ subject,
2421
+ verdict
2422
+ });
2423
+ var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2424
+ var networkIn = (parsed) => {
2425
+ if (!isRecord3(parsed)) return void 0;
2426
+ const sandbox = parsed["sandbox"];
2427
+ if (!isRecord3(sandbox)) return void 0;
2428
+ const network = sandbox["network"];
2429
+ if (!isRecord3(network)) return void 0;
2430
+ const allowed = network["allowedDomains"];
2431
+ return {
2432
+ allowedDomains: Array.isArray(allowed) ? allowed.filter((one) => typeof one === "string") : [],
2433
+ ...network["allowManagedDomainsOnly"] === true ? { allowManagedDomainsOnly: true } : {}
2434
+ };
2435
+ };
2436
+ var sourceAt = (path, label) => {
2437
+ if (!existsSync10(path)) return { network: void 0, path: label, unreadable: false };
2438
+ try {
2439
+ return {
2440
+ network: networkIn(JSON.parse(readFileSync13(path, "utf8"))),
2441
+ path: label,
2442
+ unreadable: false
2443
+ };
2444
+ } catch {
2445
+ return { network: void 0, path: label, unreadable: true };
2446
+ }
2447
+ };
2448
+ var declaredEgress = (root) => {
2449
+ const at = join13(root, GEONOSIS2);
2450
+ if (!existsSync10(at)) return void 0;
2451
+ let parsed;
2452
+ try {
2453
+ parsed = JSON.parse(readFileSync13(at, "utf8"));
2454
+ } catch {
2455
+ return void 0;
2456
+ }
2457
+ const egress = parsed?.rails?.egress;
2458
+ if (!isRecord3(egress)) return void 0;
2459
+ const allow = egress["allow"];
2460
+ return {
2461
+ allow: Array.isArray(allow) ? allow.filter((one) => typeof one === "string") : []
2462
+ };
2463
+ };
2464
+ var deniedEgress = (root) => {
2465
+ const at = join13(root, RUN_RECORD);
2466
+ if (!existsSync10(at)) {
2467
+ return [
2468
+ finding10(
2469
+ "deniedEgress",
2470
+ "SKIP",
2471
+ `there is no ${RUN_RECORD} here, so no run has been recorded for this to be a gate over`
2472
+ )
2473
+ ];
2474
+ }
2475
+ let parsed;
2476
+ try {
2477
+ parsed = JSON.parse(readFileSync13(at, "utf8"));
2478
+ } catch (error) {
2479
+ return [
2480
+ finding10(
2481
+ "deniedEgress",
2482
+ "FAIL",
2483
+ `${RUN_RECORD} is not readable JSON (${error.message}) \u2014 a record nobody can read is not a record of zero denied attempts`
2484
+ )
2485
+ ];
2486
+ }
2487
+ const denied = isRecord3(parsed) && Array.isArray(parsed["deniedEgress"]) ? parsed["deniedEgress"] : [];
2488
+ if (denied.length === 0) {
2489
+ return [finding10("deniedEgress", "OK", "no run recorded a denied egress attempt")];
2490
+ }
2491
+ const hosts = denied.map((one) => isRecord3(one) && typeof one["host"] === "string" ? one["host"] : "(unnamed)").join(", ");
2492
+ return [
2493
+ finding10(
2494
+ "deniedEgress",
2495
+ "FAIL",
2496
+ `${denied.length} denied egress attempt(s) in this run's record \u2014 ${hosts}. This is a gate at ZERO and never a counter to hold flat: a run that reached for a host this repo does not allow is a run somebody reads, whatever the number was yesterday`
2497
+ )
2498
+ ];
2499
+ };
2500
+ var checkRails = ({
2501
+ managedSettings = managedSettingsPath(),
2502
+ root,
2503
+ userSettings
2504
+ }) => {
2505
+ const declared = declaredEgress(root);
2506
+ if (declared === void 0 || declared.allow.length === 0) {
2507
+ return [
2508
+ finding10(
2509
+ `${GEONOSIS2} \u2192 rails.egress`,
2510
+ "SKIP",
2511
+ "this repo declares no egress allowlist, so there is no rendered setting for this to read back"
2512
+ )
2513
+ ];
2514
+ }
2515
+ const sources = [
2516
+ sourceAt(managedSettings, managedSettings),
2517
+ sourceAt(userSettings, userSettings),
2518
+ sourceAt(join13(root, PROJECT), PROJECT),
2519
+ sourceAt(join13(root, LOCAL), LOCAL)
2520
+ ];
2521
+ const unreadable = sources.filter((one) => one.unreadable).map(
2522
+ (one) => finding10(
2523
+ one.path,
2524
+ "FAIL",
2525
+ "is not readable JSON, and Claude Code SILENTLY ignores a settings file that fails validation \u2014 every setting rendered into this file loads as nothing, with no error anywhere. Fix the file, then render the allowlist again"
2526
+ )
2527
+ );
2528
+ const managedNetwork = sources[0]?.network;
2529
+ const onlyManaged = managedNetwork?.allowManagedDomainsOnly === true;
2530
+ const effective = new Set(
2531
+ (onlyManaged ? [sources[0]] : sources).flatMap((one) => one?.network?.allowedDomains ?? [])
2532
+ );
2533
+ const why = onlyManaged ? ` Managed settings set sandbox.network.allowManagedDomainsOnly, so only ${managedSettings} allow rules load and every project, user and local domain is ignored \u2014 this one has to be added THERE` : ` Render it with \`geonosis-rails egress --write\`, into ${PROJECT} or the user scope`;
2534
+ return [
2535
+ ...unreadable,
2536
+ ...declared.allow.map(
2537
+ (domain) => effective.has(domain) ? finding10(domain, "OK", "declared, and in the settings that actually load") : finding10(
2538
+ domain,
2539
+ "FAIL",
2540
+ `declared in ${GEONOSIS2} \u2192 rails.egress and absent from every settings file that loads, so the run has no allowance for it.${why}`
2541
+ )
2542
+ ),
2543
+ ...deniedEgress(root)
2544
+ ];
2545
+ };
2546
+
2547
+ // src/required-options.ts
2548
+ import { pathToFileURL as pathToFileURL2 } from "url";
2549
+ var HEADER = /requires option `([^`]+)`/;
2550
+ var optionsOf2 = (level) => Array.isArray(level) ? level.slice(1) : [];
2551
+ var missingOption = (error) => {
2552
+ const named2 = error.option;
2553
+ return typeof named2 === "string" ? named2 : HEADER.exec(String(error.message))?.[1];
2554
+ };
2555
+ var configured = (config, namespace) => [config.rules, ...config.overrides.map((one) => one.rules)].flatMap(
2556
+ (rules) => enabledRulesOf(rules, namespace).map((id) => [id, optionsOf2(rules[id])])
2557
+ );
2558
+ var checkRequiredOptions = async ({
2559
+ config,
2560
+ entry
2561
+ }) => {
2562
+ const loaded = await import(pathToFileURL2(entry).href).catch(() => void 0);
2563
+ const namespace = loaded?.default?.meta?.name;
2564
+ const rules = loaded?.default?.rules;
2565
+ if (typeof namespace !== "string" || namespace === "" || rules === void 0) return [];
2566
+ return configured(config, namespace).flatMap(([id, options]) => {
2567
+ const rule = rules[id.slice(namespace.length + 1)];
2568
+ if (rule === void 0) return [];
2569
+ try {
2570
+ rule.create({ options, report: () => {
2571
+ } });
2572
+ return [];
2573
+ } catch (error) {
2574
+ const option = missingOption(error);
2575
+ if (option === void 0) return [];
2576
+ return [
2577
+ {
2578
+ check: "loaded",
2579
+ message: `it is enabled here with no \`${option}\`, and the rule refuses to be constructed without one \u2014 oxlint stops the whole run at load, linting nothing. Configure \`${option}\`, or turn the rule off`,
2580
+ subject: `${config.relative} \u2192 ${id}`,
2581
+ verdict: "FAIL"
2582
+ }
2583
+ ];
2584
+ }
2585
+ });
2586
+ };
2587
+
2588
+ // src/doctor.ts
1894
2589
  var exercisedOf = ({
1895
2590
  configs,
1896
2591
  oxlint,
1897
2592
  repoCorpus,
1898
- root
2593
+ root,
2594
+ workspaces
1899
2595
  }) => Promise.all(
1900
2596
  configs.flatMap(
1901
2597
  (config) => config.jsPlugins.filter((name) => name.startsWith(SCOPE)).map(async (specifier) => {
@@ -1905,22 +2601,33 @@ var exercisedOf = ({
1905
2601
  corpus = corpusOfPlugin(config.dir, specifier);
1906
2602
  entry = resolveFrom(config.dir, specifier);
1907
2603
  } catch (error) {
1908
- return {
1909
- check: "exercised",
1910
- message: `${specifier}: ${String(error.message)}`,
1911
- subject: config.relative,
1912
- verdict: "SKIP"
1913
- };
2604
+ return [
2605
+ {
2606
+ check: "exercised",
2607
+ message: `${specifier}: ${String(error.message)}`,
2608
+ subject: config.relative,
2609
+ verdict: "SKIP"
2610
+ }
2611
+ ];
1914
2612
  }
1915
2613
  const own = repoCorpus !== void 0 && config.relative === CONFIG_FILE ? repoCorpus : void 0;
1916
- return checkExercised({
1917
- config,
1918
- corpus,
1919
- entry,
1920
- oxlint,
1921
- ...own === void 0 ? {} : { repoCorpus: own },
1922
- root
1923
- });
2614
+ const [reach, engines, options] = await Promise.all([
2615
+ checkExercised({
2616
+ config,
2617
+ corpus,
2618
+ entry,
2619
+ oxlint,
2620
+ ...own === void 0 ? {} : { repoCorpus: own },
2621
+ root
2622
+ }),
2623
+ // #159: the rules whose engine this tree has not got. Beside the reach line rather than
2624
+ // inside it — one is about the corpus, the other about this repo's own manifests.
2625
+ checkEngines({ config, entry, workspaces }),
2626
+ // #202: and the rules this config enables without an option they require, which the
2627
+ // lint can only report as a run it could not make.
2628
+ checkRequiredOptions({ config, entry })
2629
+ ]);
2630
+ return [reach, ...engines, ...options];
1924
2631
  })
1925
2632
  )
1926
2633
  );
@@ -1939,6 +2646,7 @@ var baselineOf = ({
1939
2646
  const ref = baseline.ref ?? defaultRef(root);
1940
2647
  return ref === void 0 ? skip("no ref was named and this repo has no origin/main to fall back on") : checkBaseline({ ref, root });
1941
2648
  };
2649
+ var USER_SETTINGS = ".claude/settings.json";
1942
2650
  var ordered = (findings) => CHECKS.flatMap((check) => findings.filter((one) => one.check === check));
1943
2651
  var EMPTY = { FAIL: 0, OK: 0, SKIP: 0, UNJUDGED: 0, WARN: 0 };
1944
2652
  var countsOf = (findings) => findings.reduce((counts, one) => ({ ...counts, [one.verdict]: counts[one.verdict] + 1 }), {
@@ -1961,6 +2669,17 @@ var runDoctor = async ({
1961
2669
  const asked = (check) => only === void 0 || only.includes(check);
1962
2670
  const runs = [
1963
2671
  ["loaded", () => checkLoaded({ configs, root, workspaces })],
2672
+ [
2673
+ "group",
2674
+ () => {
2675
+ const declared = declaredGroupsOf(root);
2676
+ return checkGroup({
2677
+ ...declared === void 0 ? {} : { groups: declared },
2678
+ root,
2679
+ workspaces
2680
+ });
2681
+ }
2682
+ ],
1964
2683
  [
1965
2684
  "exercised",
1966
2685
  () => {
@@ -1969,16 +2688,18 @@ var runDoctor = async ({
1969
2688
  configs,
1970
2689
  oxlint: oxlint ?? resolveOxlint(root),
1971
2690
  ...repoCorpus === void 0 ? {} : { repoCorpus },
1972
- root
1973
- });
2691
+ root,
2692
+ workspaces
2693
+ }).then((found2) => found2.flat());
1974
2694
  }
1975
2695
  ],
1976
2696
  ["baseline", () => baselineOf({ baseline, root })],
1977
- ["runner", () => checkRunner({ ratchet: readRatchet(root), workspaces })],
2697
+ ["runner", () => checkRunner({ ratchet: readRatchet(root), root, workspaces })],
1978
2698
  ["envelope", () => checkEnvelopes({ root })],
1979
2699
  ["observability", () => checkObservability({ now: Date.now(), root })],
1980
2700
  ["drift", () => checkDrift({ root, workspaces })],
1981
- ["deployed", () => checkDeployed({ root })]
2701
+ ["deployed", () => checkDeployed({ root })],
2702
+ ["rails", () => checkRails({ root, userSettings: join14(homedir2(), USER_SETTINGS) })]
1982
2703
  ];
1983
2704
  const collected = [];
1984
2705
  for (const [check, run] of runs) if (asked(check)) collected.push(...await run());
@@ -1999,8 +2720,10 @@ var ABOUT = {
1999
2720
  drift: "the gates that were set up and are no longer running",
2000
2721
  envelope: "every gate read as many things as it was handed",
2001
2722
  exercised: "every enabled rule fires on at least one corpus file",
2723
+ group: "the packages published on one version resolve to one version",
2002
2724
  loaded: "the plugin oxlint would load is the one the manifest pins",
2003
2725
  observability: "an exporter is configured, reachable, and something arrived through it lately",
2726
+ rails: "the egress a run is bounded by is in the settings that actually load",
2004
2727
  runner: "something reads the test runner\u2019s own report, not its exit code"
2005
2728
  };
2006
2729
  var WIDTH = 8;
@@ -2049,20 +2772,26 @@ export {
2049
2772
  pluginVersionOf,
2050
2773
  corpusOfPlugin,
2051
2774
  relativeToRoot,
2775
+ COMPOSITION_ROOT,
2776
+ FLOOR_PACKAGES,
2052
2777
  READERS,
2053
2778
  checkDrift,
2779
+ enabledRulesOf,
2780
+ checkExercised,
2054
2781
  ENVELOPES_DIR,
2055
2782
  NO_ENVELOPES,
2056
2783
  checkEnvelopes,
2057
- enabledRulesOf,
2058
- checkExercised,
2784
+ GEONOSIS_FILE,
2785
+ repoCorpusOf,
2786
+ FIXED_GROUP,
2787
+ KIT_GROUP,
2788
+ declaredGroupsOf,
2789
+ checkGroup,
2059
2790
  SCOPE,
2060
2791
  satisfies,
2061
2792
  declaredFor,
2062
2793
  checkLoaded,
2063
2794
  checkObservability,
2064
- GEONOSIS_FILE2 as GEONOSIS_FILE,
2065
- repoCorpusOf,
2066
2795
  checkRunner,
2067
2796
  runDoctor,
2068
2797
  formatDoctor,