@geonosis/doctor 2.0.0 → 2.1.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.
@@ -319,6 +319,22 @@ var checkDeployed = ({ root }) => {
319
319
  ];
320
320
  };
321
321
 
322
+ // src/repo-corpus.ts
323
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
324
+ import { join as join3 } from "path";
325
+ var GEONOSIS_FILE = "geonosis.json";
326
+ var repoCorpusOf = (root) => {
327
+ const path = join3(root, GEONOSIS_FILE);
328
+ if (!existsSync2(path)) return void 0;
329
+ try {
330
+ const parsed = JSON.parse(readFileSync3(path, "utf8"));
331
+ const declared = parsed.doctor?.corpus;
332
+ return typeof declared === "string" && declared !== "" ? join3(root, declared) : void 0;
333
+ } catch {
334
+ return void 0;
335
+ }
336
+ };
337
+
322
338
  // src/types.ts
323
339
  var CHECKS = [
324
340
  "loaded",
@@ -340,9 +356,9 @@ var DoctorError = class extends Error {
340
356
  };
341
357
 
342
358
  // src/resolve.ts
343
- import { existsSync as existsSync2, readFileSync as readFileSync3, realpathSync } from "fs";
359
+ import { existsSync as existsSync3, readFileSync as readFileSync4, realpathSync } from "fs";
344
360
  import { createRequire } from "module";
345
- import { dirname, join as join3 } from "path";
361
+ import { dirname, join as join4 } from "path";
346
362
  import { pathToFileURL } from "url";
347
363
  var packageNameOf = (specifier) => {
348
364
  const parts2 = specifier.split("/");
@@ -350,13 +366,13 @@ var packageNameOf = (specifier) => {
350
366
  };
351
367
  var resolveFrom = (dir, specifier) => {
352
368
  if (specifier.startsWith(".") || specifier.startsWith("/")) {
353
- return createRequire(join3(dir, "noop.js")).resolve(specifier);
369
+ return createRequire(join4(dir, "noop.js")).resolve(specifier);
354
370
  }
355
371
  const name = packageNameOf(specifier);
356
372
  let at = dir;
357
373
  for (; ; ) {
358
- if (existsSync2(join3(at, "node_modules", name, "package.json"))) {
359
- return createRequire(join3(at, "noop.js")).resolve(specifier);
374
+ if (existsSync3(join4(at, "node_modules", name, "package.json"))) {
375
+ return createRequire(join4(at, "noop.js")).resolve(specifier);
360
376
  }
361
377
  const parent = dirname(at);
362
378
  if (parent === at) break;
@@ -369,10 +385,10 @@ var resolveFrom = (dir, specifier) => {
369
385
  var packageDirOf = (entry, name) => {
370
386
  let dir = dirname(entry);
371
387
  for (; ; ) {
372
- const manifest = join3(dir, "package.json");
373
- if (existsSync2(manifest)) {
388
+ const manifest = join4(dir, "package.json");
389
+ if (existsSync3(manifest)) {
374
390
  try {
375
- const parsed = JSON.parse(readFileSync3(manifest, "utf8"));
391
+ const parsed = JSON.parse(readFileSync4(manifest, "utf8"));
376
392
  if (parsed.name === name) return dir;
377
393
  } catch {
378
394
  }
@@ -421,7 +437,7 @@ var presumptionsOf = async (entry) => {
421
437
  )
422
438
  };
423
439
  };
424
- var corpusOfPlugin = (from, specifier) => join3(packageDirOf(resolveFrom(from, specifier), specifier), "corpus");
440
+ var corpusOfPlugin = (from, specifier) => join4(packageDirOf(resolveFrom(from, specifier), specifier), "corpus");
425
441
  var real = (path) => {
426
442
  try {
427
443
  return realpathSync(path);
@@ -431,14 +447,13 @@ var real = (path) => {
431
447
  };
432
448
  var relativeToRoot = (root, path) => relativePath(real(root), real(path));
433
449
 
434
- // src/drift.ts
435
- import { closeSync, existsSync as existsSync4, openSync, readdirSync as readdirSync3, readFileSync as readFileSync5, readSync } from "fs";
436
- import { homedir } from "os";
437
- import { join as join5, sep as sep2 } from "path";
450
+ // src/group.ts
451
+ import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
452
+ import { join as join6 } from "path";
438
453
 
439
454
  // src/hooks.ts
440
- import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
441
- import { join as join4 } from "path";
455
+ import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync5 } from "fs";
456
+ import { join as join5 } from "path";
442
457
  var HOOK_FILES = ["lefthook.yml", "lefthook.yaml", ".lefthook.yml", ".lefthook.yaml"];
443
458
  var HOOK_DIRS = [".husky", ".githooks"];
444
459
  var VERSION_MANAGED = /(?:^|[\s;&|(])(?<runner>pnpm|npx|bunx|yarn)\b(?:\s+(?:exec|run|dlx|x))?\s+(?<bin>geonosis(?:-[a-z-]+)?)\b/;
@@ -447,26 +462,267 @@ var hookLines = (root) => {
447
462
  const found = [];
448
463
  const read = (path) => {
449
464
  const at = relativePath(root, path);
450
- for (const line of readFileSync4(path, "utf8").split("\n")) {
465
+ for (const line of readFileSync5(path, "utf8").split("\n")) {
451
466
  if (line.trim() !== "") found.push({ at, line });
452
467
  }
453
468
  };
454
469
  for (const name of HOOK_FILES) {
455
- const path = join4(root, name);
456
- if (existsSync3(path)) read(path);
470
+ const path = join5(root, name);
471
+ if (existsSync4(path)) read(path);
457
472
  }
458
473
  for (const name of HOOK_DIRS) {
459
- const dir = join4(root, name);
460
- if (!existsSync3(dir)) continue;
474
+ const dir = join5(root, name);
475
+ if (!existsSync4(dir)) continue;
461
476
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
462
477
  if (entry.isFile() && !entry.name.startsWith("_") && !entry.name.startsWith(".")) {
463
- read(join4(dir, entry.name));
478
+ read(join5(dir, entry.name));
464
479
  }
465
480
  }
466
481
  }
467
482
  return found;
468
483
  };
469
484
 
485
+ // src/group.ts
486
+ var FIXED_GROUP = [
487
+ "@geonosis/cli",
488
+ "@geonosis/doctor",
489
+ "@geonosis/evals",
490
+ "@geonosis/integrations",
491
+ "@geonosis/ledger",
492
+ "@geonosis/lint-parity",
493
+ "@geonosis/mcp",
494
+ "@geonosis/observability",
495
+ "@geonosis/oxlint-plugin-biological-architecture",
496
+ "@geonosis/policy",
497
+ "@geonosis/rails",
498
+ "@geonosis/ratchet",
499
+ "@geonosis/release",
500
+ "@geonosis/review",
501
+ "@geonosis/testbed",
502
+ "@geonosis/themekit",
503
+ "@geonosis/verify",
504
+ "@geonosis/verify-arch",
505
+ "@geonosis/visual-diff",
506
+ "@geonosis/walk",
507
+ "create-geonosis",
508
+ "geonosis"
509
+ ];
510
+ var KIT_GROUP = { name: "@geonosis fixed group", packages: FIXED_GROUP };
511
+ var declaredGroupsOf = (root) => {
512
+ const path = join6(root, GEONOSIS_FILE);
513
+ if (!existsSync5(path)) return void 0;
514
+ let parsed;
515
+ try {
516
+ parsed = JSON.parse(readFileSync6(path, "utf8"));
517
+ } catch {
518
+ return void 0;
519
+ }
520
+ const declared = parsed.doctor?.groups;
521
+ if (declared === void 0) return void 0;
522
+ if (!Array.isArray(declared) || declared.length === 0) {
523
+ throw new DoctorError(
524
+ `${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`
525
+ );
526
+ }
527
+ return declared.map((one, at) => {
528
+ const group = one;
529
+ if (typeof group.name !== "string" || !Array.isArray(group.packages) || group.packages.length < 2) {
530
+ throw new DoctorError(
531
+ `${GEONOSIS_FILE} doctor.groups[${at}] needs a name and at least two packages \u2014 one package cannot disagree with itself`
532
+ );
533
+ }
534
+ return { name: group.name, packages: group.packages };
535
+ });
536
+ };
537
+ var labelOf = (workspace) => workspace.relative === "" ? "root" : workspace.relative;
538
+ var versionOf = (dir, name) => {
539
+ let entry;
540
+ try {
541
+ entry = resolveFrom(dir, name);
542
+ } catch {
543
+ return { kind: "absent" };
544
+ }
545
+ try {
546
+ const manifest = JSON.parse(
547
+ readFileSync6(join6(packageDirOf(entry, name), "package.json"), "utf8")
548
+ );
549
+ return typeof manifest.version === "string" ? { kind: "read", version: manifest.version } : { kind: "unreadable", why: "its package.json declares no version" };
550
+ } catch (error) {
551
+ return { kind: "unreadable", why: String(error.message).split("\n")[0] ?? "" };
552
+ }
553
+ };
554
+ var finding3 = (subject, verdict, message) => ({
555
+ check: "group",
556
+ message,
557
+ subject,
558
+ verdict
559
+ });
560
+ var said = (versions) => [...versions.entries()].map(([version, from]) => `${version} (${from.join(", ")})`).join("; ");
561
+ var repoWide = (name, found) => {
562
+ const versions = /* @__PURE__ */ new Map();
563
+ for (const one of found) versions.set(one.version, [...versions.get(one.version) ?? [], one.at]);
564
+ if (versions.size === 1) return void 0;
565
+ return finding3(
566
+ name,
567
+ "FAIL",
568
+ `${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`
569
+ );
570
+ };
571
+ var perWorkspace = (group, at, found) => {
572
+ if (new Set(found.map((one) => one.version)).size === 1) return void 0;
573
+ const listed = found.map((one) => `${one.name} ${one.version}`).join(", ");
574
+ return finding3(
575
+ at,
576
+ "FAIL",
577
+ `${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`
578
+ );
579
+ };
580
+ var DOORS = ["@geonosis/cli", "geonosis"];
581
+ var manifestOf = (dir, name) => {
582
+ try {
583
+ return JSON.parse(
584
+ readFileSync6(join6(packageDirOf(resolveFrom(dir, name), name), "package.json"), "utf8")
585
+ );
586
+ } catch {
587
+ return void 0;
588
+ }
589
+ };
590
+ var binNamesOf = (manifest, name) => {
591
+ const declared = manifest?.bin;
592
+ return typeof declared === "string" ? [name.replace(/^@[^/]+\//, "")] : Object.keys(declared ?? {});
593
+ };
594
+ var declaredIn2 = (manifest) => [
595
+ ...new Set(
596
+ [
597
+ manifest.dependencies,
598
+ manifest.devDependencies,
599
+ manifest.optionalDependencies,
600
+ manifest.peerDependencies
601
+ ].flatMap((block) => Object.keys(block ?? {}))
602
+ )
603
+ ];
604
+ var calls = (lines, bin) => new RegExp(`(?:^|[\\s;&|(/])${bin}(?:\\s|$)`).test(lines);
605
+ var workspaceCaller = (workspaces, bins) => {
606
+ for (const workspace of workspaces) {
607
+ if (workspace.relative === "") continue;
608
+ for (const [script, line] of Object.entries(workspace.manifest.scripts ?? {})) {
609
+ if (bins.some((bin) => calls(line, bin))) return { at: workspace.relative, script };
610
+ }
611
+ }
612
+ return void 0;
613
+ };
614
+ var rootDeclarations = (root, workspaces) => {
615
+ const here = workspaces.find((one) => one.relative === "");
616
+ if (here === void 0) return [];
617
+ const declared = declaredIn2(here.manifest);
618
+ const door = DOORS.find((name) => declared.includes(name));
619
+ if (door === void 0) return [];
620
+ const owned = new Set(
621
+ [door, ...DOORS].flatMap((name) => {
622
+ const manifest = manifestOf(root, name);
623
+ return manifest === void 0 ? [] : Object.keys(manifest.dependencies ?? {});
624
+ })
625
+ );
626
+ const callers = [
627
+ ...Object.values(here.manifest.scripts ?? {}),
628
+ ...hookLines(root).map((one) => one.line)
629
+ ].join("\n");
630
+ const loadedHere = new Set(readConfig(join6(root, CONFIG_FILE), root).jsPlugins);
631
+ const spare = declared.filter(
632
+ (name) => !DOORS.includes(name) && owned.has(name) && !loadedHere.has(name) && !binNamesOf(manifestOf(root, name), name).some((bin) => calls(callers, bin))
633
+ );
634
+ if (spare.length === 0) {
635
+ return [
636
+ finding3(
637
+ "package.json",
638
+ "OK",
639
+ `every @geonosis package this root declares is ${door} or a bin its own scripts call by name`
640
+ )
641
+ ];
642
+ }
643
+ return spare.map((name) => {
644
+ const bins = binNamesOf(manifestOf(root, name), name);
645
+ const inWorkspace = workspaceCaller(workspaces, bins);
646
+ if (inWorkspace !== void 0) {
647
+ return finding3(
648
+ name,
649
+ "OK",
650
+ `no root script calls its bin, and ${inWorkspace.at}'s "${inWorkspace.script}" script does \u2014 under a hoisted linker the ROOT declaration is what makes that bin resolvable there, so this is the declaration doing its job`
651
+ );
652
+ }
653
+ return finding3(
654
+ name,
655
+ "WARN",
656
+ `the root declares it and no script in this repo 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`
657
+ );
658
+ });
659
+ };
660
+ var checkGroup = ({
661
+ groups,
662
+ root,
663
+ workspaces
664
+ }) => {
665
+ const findings = [...rootDeclarations(root, workspaces)];
666
+ for (const group of groups ?? [KIT_GROUP]) {
667
+ const found = [];
668
+ for (const workspace of workspaces) {
669
+ const at = labelOf(workspace);
670
+ const here = [];
671
+ for (const name of group.packages) {
672
+ const reading = versionOf(workspace.dir, name);
673
+ if (reading.kind === "read") here.push({ at, name, version: reading.version });
674
+ if (reading.kind === "unreadable") {
675
+ findings.push(
676
+ finding3(
677
+ `${at} \u2192 ${name}`,
678
+ "UNJUDGED",
679
+ `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`
680
+ )
681
+ );
682
+ }
683
+ }
684
+ const split2 = here.length > 0 ? perWorkspace(group, at, here) : void 0;
685
+ if (split2 !== void 0) findings.push(split2);
686
+ found.push(...here);
687
+ }
688
+ if (found.length === 0) {
689
+ findings.push(
690
+ finding3(
691
+ group.name,
692
+ "SKIP",
693
+ `no workspace under ${root} resolves any of ${group.packages.join(", ")}, so nothing here versions together`
694
+ )
695
+ );
696
+ continue;
697
+ }
698
+ const names = [...new Set(found.map((one) => one.name))].toSorted();
699
+ const split = names.flatMap((name) => {
700
+ const line = repoWide(
701
+ name,
702
+ found.filter((one) => one.name === name)
703
+ );
704
+ return line === void 0 ? [] : [line];
705
+ });
706
+ findings.push(...split);
707
+ if (split.length === 0) {
708
+ const versions = [...new Set(found.map((one) => one.version))];
709
+ findings.push(
710
+ finding3(
711
+ group.name,
712
+ "OK",
713
+ `${names.length} of ${group.packages.length} installed, at ${versions.join(", ")}, one copy each across ${new Set(found.map((one) => one.at)).size} workspaces`
714
+ )
715
+ );
716
+ }
717
+ }
718
+ return findings;
719
+ };
720
+
721
+ // src/drift.ts
722
+ import { closeSync, existsSync as existsSync6, openSync, readdirSync as readdirSync3, readFileSync as readFileSync7, readSync } from "fs";
723
+ import { homedir } from "os";
724
+ import { join as join7, sep as sep2 } from "path";
725
+
470
726
  // src/overrides.ts
471
727
  var SPECIAL = /* @__PURE__ */ new Set(["$", "(", ")", "+", ".", "/", "@", "\\", "^", "|"]);
472
728
  var globToRegExp = (glob) => {
@@ -537,7 +793,7 @@ var GEONOSIS = "geonosis.json";
537
793
  var LAW = "CLAUDE.md";
538
794
  var CEILING = 200;
539
795
  var SWITCHED_OFF = /^\s*if:\s*(?:\$\{\{\s*)?false\b/m;
540
- var finding3 = (subject, verdict, message) => ({
796
+ var finding4 = (subject, verdict, message) => ({
541
797
  check: "drift",
542
798
  message,
543
799
  subject,
@@ -555,27 +811,27 @@ var filesUnder = (dir, match) => {
555
811
  }
556
812
  for (const entry of entries) {
557
813
  if (entry.isDirectory()) {
558
- if (!entry.name.startsWith(".") && !NEVER_WALKED2.has(entry.name)) walk2(join5(at, entry.name));
814
+ if (!entry.name.startsWith(".") && !NEVER_WALKED2.has(entry.name)) walk2(join7(at, entry.name));
559
815
  continue;
560
816
  }
561
- if (entry.isFile() && match(entry.name)) found.push(join5(at, entry.name));
817
+ if (entry.isFile() && match(entry.name)) found.push(join7(at, entry.name));
562
818
  }
563
819
  };
564
820
  walk2(dir);
565
821
  return found;
566
822
  };
567
823
  var ci = (root) => {
568
- const dir = join5(root, WORKFLOWS);
569
- if (!existsSync4(dir)) {
570
- return [finding3(WORKFLOWS, "SKIP", "there are no workflows here to read")];
824
+ const dir = join7(root, WORKFLOWS);
825
+ if (!existsSync6(dir)) {
826
+ return [finding4(WORKFLOWS, "SKIP", "there are no workflows here to read")];
571
827
  }
572
828
  return filesUnder(dir, (name) => name.endsWith(".yml") || name.endsWith(".yaml")).map((path) => {
573
829
  const at = relativePath(root, path);
574
- return SWITCHED_OFF.test(readFileSync5(path, "utf8")) ? finding3(
830
+ return SWITCHED_OFF.test(readFileSync7(path, "utf8")) ? finding4(
575
831
  at,
576
832
  "FAIL",
577
833
  "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"
578
- ) : finding3(at, "OK", "nothing in it is switched off");
834
+ ) : finding4(at, "OK", "nothing in it is switched off");
579
835
  });
580
836
  };
581
837
  var holds = (parent, child) => child === parent || child.startsWith(`${parent}${sep2}`);
@@ -591,10 +847,10 @@ var orphanTests = (root, workspaces) => {
591
847
  orphaned.set(at, [...orphaned.get(at) ?? [], relativePath(root, path)]);
592
848
  }
593
849
  if (orphaned.size === 0) {
594
- return [finding3("test files", "OK", "every test file sits under a workspace that runs tests")];
850
+ return [finding4("test files", "OK", "every test file sits under a workspace that runs tests")];
595
851
  }
596
852
  return [...orphaned.entries()].map(
597
- ([at, files]) => finding3(
853
+ ([at, files]) => finding4(
598
854
  at,
599
855
  "FAIL",
600
856
  `${files.length} test file(s) here and no test script to run them \u2014 ${files.slice(0, 3).join(", ")}`
@@ -619,16 +875,16 @@ var pathsRunByScript = (script) => script.split(BETWEEN_COMMANDS).map(pathRunBy)
619
875
  var scriptPaths = (workspaces) => {
620
876
  const missing = workspaces.flatMap(
621
877
  (one) => Object.entries(one.manifest.scripts ?? {}).flatMap(
622
- ([name, script]) => pathsRunByScript(script).filter((path) => !existsSync4(join5(one.dir, path))).map((path) => ({ name, path, workspace: one }))
878
+ ([name, script]) => pathsRunByScript(script).filter((path) => !existsSync6(join7(one.dir, path))).map((path) => ({ name, path, workspace: one }))
623
879
  )
624
880
  );
625
881
  if (missing.length === 0) {
626
882
  return [
627
- finding3("script paths", "OK", "every file a script hands to bun, node or tsx is on disk")
883
+ finding4("script paths", "OK", "every file a script hands to bun, node or tsx is on disk")
628
884
  ];
629
885
  }
630
886
  return missing.map(
631
- ({ name, path, workspace }) => finding3(
887
+ ({ name, path, workspace }) => finding4(
632
888
  workspace.relative === "" ? "package.json" : `${workspace.relative}/package.json`,
633
889
  "FAIL",
634
890
  `the "${name}" script runs ${path} and there is no such file \u2014 this script fails at that command, and every gate that calls it has been running nothing since the file went`
@@ -651,16 +907,16 @@ var workspaceBins = (workspaces) => {
651
907
  var linkedBins = (root, workspaces) => {
652
908
  const bins = workspaceBins(workspaces);
653
909
  if (bins.size === 0) {
654
- return [finding3("workspace bins", "SKIP", "no workspace here declares a bin")];
910
+ return [finding4("workspace bins", "SKIP", "no workspace here declares a bin")];
655
911
  }
656
912
  const missing = workspaces.flatMap(
657
913
  (one) => Object.entries(one.manifest.scripts ?? {}).flatMap(
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 }))
914
+ ([script, body]) => [...new Set(body.split(/[\s;|&()]+/).filter((word) => bins.has(word)))].filter((name) => !existsSync6(join7(root, "node_modules/.bin", name))).map((name) => ({ name, script, workspace: one }))
659
915
  )
660
916
  );
661
917
  if (missing.length === 0) {
662
918
  return [
663
- finding3(
919
+ finding4(
664
920
  "workspace bins",
665
921
  "OK",
666
922
  "every workspace bin a script names is linked under the root"
@@ -668,7 +924,7 @@ var linkedBins = (root, workspaces) => {
668
924
  ];
669
925
  }
670
926
  return missing.map(
671
- ({ name, script, workspace }) => finding3(
927
+ ({ name, script, workspace }) => finding4(
672
928
  name,
673
929
  "FAIL",
674
930
  `the "${script}" script in ${workspace.relative === "" ? "package.json" : `${workspace.relative}/package.json`} calls it, ${bins.get(name)?.manifest.name ?? "a workspace"} declares it, and node_modules/.bin/${name} is not there \u2014 add that package as a dependency of the one whose script calls it, so the package manager links the bin`
@@ -690,7 +946,7 @@ var preloadAroundPnpm = (root, workspaces) => {
690
946
  const some = scripts.slice(0, 3).join(", ");
691
947
  const rest = scripts.length > 3 ? ` and ${scripts.length - 3} more` : "";
692
948
  return [
693
- finding3(
949
+ finding4(
694
950
  "NODE_OPTIONS",
695
951
  "WARN",
696
952
  `${scripts.length} script(s) preload something through NODE_OPTIONS (${some}${rest}), and the "${named2}" counter(s) shell out to pnpm \u2014 a nested pnpm INHERITS the option and dies on it (a preloaded resolver patch sends it looking for a .pnpmfile.mjs that is not there). Run the ratchet outside those scripts, or unset NODE_OPTIONS for the nested call: env -u NODE_OPTIONS pnpm \u2026`
@@ -726,11 +982,11 @@ var everythingRun = (root, workspaces) => {
726
982
  const scripts = workspaces.flatMap((one) => Object.values(one.manifest.scripts ?? {}));
727
983
  const counters = (readRatchet(root)?.counters ?? []).map((entry) => String(entry.command ?? ""));
728
984
  const workflows = filesUnder(
729
- join5(root, WORKFLOWS),
985
+ join7(root, WORKFLOWS),
730
986
  (name) => name.endsWith(".yml") || name.endsWith(".yaml")
731
987
  ).map((path) => {
732
988
  try {
733
- return readFileSync5(path, "utf8");
989
+ return readFileSync7(path, "utf8");
734
990
  } catch {
735
991
  return "";
736
992
  }
@@ -741,7 +997,7 @@ var generatedFiles = (root, workspaces) => {
741
997
  const marked = filesUnder(root, (name) => CAN_CARRY_A_MARKER.test(name)).map((path) => ({ marker: markerIn(path), path })).filter((one) => one.marker !== void 0);
742
998
  if (marked.length === 0) {
743
999
  return [
744
- finding3(
1000
+ finding4(
745
1001
  "generated files",
746
1002
  "SKIP",
747
1003
  "nothing here says it was generated, so there is no write half here to look for a read half of"
@@ -752,13 +1008,13 @@ var generatedFiles = (root, workspaces) => {
752
1008
  return marked.map(({ marker, path }) => {
753
1009
  const at = relativePath(root, path);
754
1010
  if (!("gate" in marker)) {
755
- return finding3(
1011
+ return finding4(
756
1012
  at,
757
1013
  "FAIL",
758
1014
  `generated by \`${marker.writer}\` and it names no gate that reads it back. A written file no check reads is the write half of an instrument with no read half: it can drift from its source for ever and every gate stays green. Re-run \`${marker.writer}\` on a version that writes the \`geonosis:gated-by:\` marker, and run the gate it names`
759
1015
  );
760
1016
  }
761
- return run.includes(marker.gate) ? finding3(at, "OK", `generated, and \`${marker.gate}\` reads it back`) : finding3(
1017
+ return run.includes(marker.gate) ? finding4(at, "OK", `generated, and \`${marker.gate}\` reads it back`) : finding4(
762
1018
  at,
763
1019
  "FAIL",
764
1020
  `generated, and nothing here runs \`${marker.gate}\` \u2014 the gate it names. A written file no check reads is the write half of an instrument with no read half: it can drift from its source for ever and every gate stays green. Add \`${marker.gate}\` to a script, a counter or a workflow`
@@ -766,10 +1022,10 @@ var generatedFiles = (root, workspaces) => {
766
1022
  });
767
1023
  };
768
1024
  var readGeonosis = (root) => {
769
- const path = join5(root, GEONOSIS);
770
- if (!existsSync4(path)) return void 0;
1025
+ const path = join7(root, GEONOSIS);
1026
+ if (!existsSync6(path)) return void 0;
771
1027
  try {
772
- return JSON.parse(readFileSync5(path, "utf8"));
1028
+ return JSON.parse(readFileSync7(path, "utf8"));
773
1029
  } catch {
774
1030
  return void 0;
775
1031
  }
@@ -777,15 +1033,15 @@ var readGeonosis = (root) => {
777
1033
  var law = (root, config) => {
778
1034
  const declared = config?.law ?? {};
779
1035
  const file = typeof declared.file === "string" ? declared.file : LAW;
780
- const path = join5(root, file);
781
- if (!existsSync4(path)) {
782
- return [finding3(file, "SKIP", "there is no law file here to measure")];
1036
+ const path = join7(root, file);
1037
+ if (!existsSync6(path)) {
1038
+ return [finding4(file, "SKIP", "there is no law file here to measure")];
783
1039
  }
784
- const source = readFileSync5(path, "utf8");
1040
+ const source = readFileSync7(path, "utf8");
785
1041
  const lines = source.split("\n").length - (source.endsWith("\n") ? 1 : 0);
786
1042
  if (typeof declared.maxLines !== "number") {
787
1043
  return [
788
- finding3(
1044
+ finding4(
789
1045
  file,
790
1046
  "SKIP",
791
1047
  `${lines} lines, and no ceiling to measure them against \u2014 set law.maxLines in geonosis.json to have this asked (${CEILING} is what both source repos converged on)`
@@ -794,18 +1050,18 @@ var law = (root, config) => {
794
1050
  }
795
1051
  const ceiling = declared.maxLines;
796
1052
  return [
797
- lines > ceiling ? finding3(
1053
+ lines > ceiling ? finding4(
798
1054
  file,
799
1055
  "WARN",
800
1056
  `${lines} lines against a ceiling of ${ceiling} \u2014 depth belongs in the skills and in the rules, where it is read`
801
- ) : finding3(file, "OK", `${lines} lines, under the ceiling of ${ceiling}`)
1057
+ ) : finding4(file, "OK", `${lines} lines, under the ceiling of ${ceiling}`)
802
1058
  ];
803
1059
  };
804
1060
  var KIT_PLUGIN = "geonosis";
805
1061
  var enablesKit = (path) => {
806
- if (!existsSync4(path)) return false;
1062
+ if (!existsSync6(path)) return false;
807
1063
  try {
808
- const parsed = JSON.parse(readFileSync5(path, "utf8"));
1064
+ const parsed = JSON.parse(readFileSync7(path, "utf8"));
809
1065
  const enabled = parsed.enabledPlugins;
810
1066
  if (typeof enabled !== "object" || enabled === null) return false;
811
1067
  return Object.entries(enabled).some(
@@ -816,13 +1072,13 @@ var enablesKit = (path) => {
816
1072
  }
817
1073
  };
818
1074
  var hooks = (root, userSettings) => {
819
- const path = join5(root, SETTINGS);
1075
+ const path = join7(root, SETTINGS);
820
1076
  if (enablesKit(path)) {
821
- return [finding3(SETTINGS, "OK", `\`enabledPlugins\` here enables the kit\u2019s plugin`)];
1077
+ return [finding4(SETTINGS, "OK", `\`enabledPlugins\` here enables the kit\u2019s plugin`)];
822
1078
  }
823
1079
  if (enablesKit(userSettings)) {
824
1080
  return [
825
- finding3(
1081
+ finding4(
826
1082
  SETTINGS,
827
1083
  "OK",
828
1084
  `installed elsewhere \u2014 not this repo\u2019s to declare: \`enabledPlugins\` in ${userSettings} enables it for every repo on this machine`
@@ -830,7 +1086,7 @@ var hooks = (root, userSettings) => {
830
1086
  ];
831
1087
  }
832
1088
  return [
833
- finding3(
1089
+ finding4(
834
1090
  SETTINGS,
835
1091
  "WARN",
836
1092
  `nothing enables the kit\u2019s plugin, so none of the hooks, agents or skills reach this repo \u2014 the gates run, the method does not. What satisfies this: a "geonosis@<marketplace>" entry under \`enabledPlugins\` in ${SETTINGS} here, or the same in the user-scope ${userSettings}`
@@ -839,9 +1095,9 @@ var hooks = (root, userSettings) => {
839
1095
  };
840
1096
  var ALLOW_BUILDS_KEY = "allowBuilds";
841
1097
  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");
1098
+ const path = join7(root, WORKSPACE_YAML);
1099
+ if (!existsSync6(path)) return void 0;
1100
+ const lines = readFileSync7(path, "utf8").split("\n");
845
1101
  const at = lines.findIndex((line) => new RegExp(`^${ALLOW_BUILDS_KEY}\\s*:`).test(line));
846
1102
  if (at < 0) return void 0;
847
1103
  const names = [];
@@ -866,7 +1122,7 @@ var platformSplitBuilds = (root) => {
866
1122
  }
867
1123
  return [...families.entries()].flatMap(
868
1124
  ([family, allowed]) => allowed.length > 1 ? [] : [
869
- finding3(
1125
+ finding4(
870
1126
  allowed[0] ?? family,
871
1127
  "WARN",
872
1128
  `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`
@@ -892,7 +1148,7 @@ var pathsLintedBy = (script, dir) => {
892
1148
  if (word.startsWith("-")) continue;
893
1149
  const before = words[at - 1] ?? "";
894
1150
  if (TAKES_A_VALUE.has(before)) continue;
895
- if (existsSync4(join5(dir, word))) named2.push(word.replace(/^\.\//, "").replace(/\/$/, ""));
1151
+ if (existsSync6(join7(dir, word))) named2.push(word.replace(/^\.\//, "").replace(/\/$/, ""));
896
1152
  }
897
1153
  return named2;
898
1154
  };
@@ -905,13 +1161,13 @@ var sourceDirsIn = (dir) => {
905
1161
  return [];
906
1162
  }
907
1163
  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
1164
+ (entry) => entry.isDirectory() && !entry.name.startsWith(".") && !NEVER_WALKED2.has(entry.name) && filesUnder(join7(dir, entry.name), (name) => LINTABLE.test(name)).length > 0
909
1165
  ).map((entry) => entry.name);
910
1166
  };
911
1167
  var lintScopes = (root, workspaces) => {
912
1168
  if (!hookLines(root).some((one) => LINTS_STAGED.test(one.line))) {
913
1169
  return [
914
- finding3(
1170
+ finding4(
915
1171
  "lint scope",
916
1172
  "SKIP",
917
1173
  "no committed hook lints staged files here, so there is no second file set for a lint script to be narrower than"
@@ -924,16 +1180,16 @@ var lintScopes = (root, workspaces) => {
924
1180
  const at = workspace.relative === "" ? "package.json" : `${workspace.relative}/package.json`;
925
1181
  const covered = pathsLintedBy(script, workspace.dir);
926
1182
  if (covered.length === 0 || covered.includes(".")) {
927
- return [finding3(at, "OK", `"${script}" covers everything the hook could stage here`)];
1183
+ return [finding4(at, "OK", `"${script}" covers everything the hook could stage here`)];
928
1184
  }
929
1185
  const uncovered = sourceDirsIn(workspace.dir).filter(
930
1186
  (name) => !covered.some((path) => name === path || path.startsWith(`${name}/`))
931
1187
  );
932
1188
  if (uncovered.length === 0) {
933
- return [finding3(at, "OK", `"${script}" covers every source directory under this workspace`)];
1189
+ return [finding4(at, "OK", `"${script}" covers every source directory under this workspace`)];
934
1190
  }
935
1191
  return [
936
- finding3(
1192
+ finding4(
937
1193
  at,
938
1194
  "WARN",
939
1195
  `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`
@@ -969,7 +1225,7 @@ var composedHere = (root, workspaces) => {
969
1225
  for (const path of filesUnder(root, (name) => SOURCE_FILE.test(name))) {
970
1226
  let body;
971
1227
  try {
972
- body = readFileSync5(path, "utf8");
1228
+ body = readFileSync7(path, "utf8");
973
1229
  } catch {
974
1230
  continue;
975
1231
  }
@@ -980,11 +1236,11 @@ var composedHere = (root, workspaces) => {
980
1236
  }
981
1237
  return wanted.map((one) => {
982
1238
  const at = importing.get(one.name);
983
- return at === void 0 ? finding3(
1239
+ return at === void 0 ? finding4(
984
1240
  one.name,
985
1241
  "WARN",
986
1242
  `it is declared here and no source file imports it \u2014 ${one.uncomposed}`
987
- ) : finding3(
1243
+ ) : finding4(
988
1244
  one.name,
989
1245
  "OK",
990
1246
  `composed in ${relativePath(root, at)} \u2014 something here builds what it reports through`
@@ -998,6 +1254,18 @@ var READERS = {
998
1254
  verify: "@geonosis/verify",
999
1255
  walk: "@geonosis/walk"
1000
1256
  };
1257
+ var doorBringing = (root, name, declared) => DOORS.filter((door) => declared.has(door)).find(
1258
+ (door) => Object.hasOwn(dependenciesOf(root, door), name)
1259
+ );
1260
+ var dependenciesOf = (root, name) => {
1261
+ try {
1262
+ return JSON.parse(
1263
+ readFileSync7(join7(packageDirOf(resolveFrom(root, name), name), "package.json"), "utf8")
1264
+ ).dependencies ?? {};
1265
+ } catch {
1266
+ return {};
1267
+ }
1268
+ };
1001
1269
  var resolves = (root, name) => {
1002
1270
  try {
1003
1271
  resolveFrom(root, name);
@@ -1019,7 +1287,7 @@ var declaredAnywhere = (workspaces) => new Set(
1019
1287
  var blocks = (root, config, readers, workspaces) => {
1020
1288
  if (config === void 0) {
1021
1289
  return [
1022
- finding3(
1290
+ finding4(
1023
1291
  GEONOSIS,
1024
1292
  "SKIP",
1025
1293
  "there is no geonosis.json here, so no block says what this repo asks the kit to do"
@@ -1029,11 +1297,21 @@ var blocks = (root, config, readers, workspaces) => {
1029
1297
  const declaredPackages = declaredAnywhere(workspaces);
1030
1298
  return Object.entries(readers).flatMap(([block, name]) => {
1031
1299
  const declared = config[block] !== void 0;
1300
+ const door = doorBringing(root, name, declaredPackages);
1032
1301
  const chosen = declaredPackages.has(name);
1033
1302
  const installed = resolves(root, name);
1303
+ if (declared && !chosen && door !== void 0) {
1304
+ return [
1305
+ finding4(
1306
+ name,
1307
+ "OK",
1308
+ `a "${block}" block, and ${door} brings ${name} to read it \u2014 #177: the root declares the door, not what the door owns`
1309
+ )
1310
+ ];
1311
+ }
1034
1312
  if (declared && !chosen) {
1035
1313
  return [
1036
- finding3(
1314
+ finding4(
1037
1315
  name,
1038
1316
  "WARN",
1039
1317
  `geonosis.json has a "${block}" block and no manifest here declares ${name} \u2014 nothing reads it${installed ? ", and the copy still under node_modules is what a stale install left behind" : ""}`
@@ -1042,7 +1320,7 @@ var blocks = (root, config, readers, workspaces) => {
1042
1320
  }
1043
1321
  if (declared && !installed) {
1044
1322
  return [
1045
- finding3(
1323
+ finding4(
1046
1324
  name,
1047
1325
  "WARN",
1048
1326
  `geonosis.json has a "${block}" block, a manifest declares ${name}, and it does not resolve here \u2014 run the install`
@@ -1051,7 +1329,7 @@ var blocks = (root, config, readers, workspaces) => {
1051
1329
  }
1052
1330
  if (!declared && chosen && installed) {
1053
1331
  return [
1054
- finding3(
1332
+ finding4(
1055
1333
  name,
1056
1334
  "WARN",
1057
1335
  `${name} is installed and geonosis.json has no "${block}" block \u2014 it runs on its defaults, whatever they are`
@@ -1060,14 +1338,14 @@ var blocks = (root, config, readers, workspaces) => {
1060
1338
  }
1061
1339
  if (!declared && installed) {
1062
1340
  return [
1063
- finding3(
1341
+ finding4(
1064
1342
  name,
1065
1343
  "OK",
1066
1344
  `${name} is here but no manifest declares it \u2014 a transitive dependency this repo did not choose, so it wants no "${block}" block`
1067
1345
  )
1068
1346
  ];
1069
1347
  }
1070
- return declared ? [finding3(name, "OK", `a "${block}" block, and ${name} to read it`)] : [];
1348
+ return declared ? [finding4(name, "OK", `a "${block}" block, and ${name} to read it`)] : [];
1071
1349
  });
1072
1350
  };
1073
1351
  var PLUGIN_DIR_RULE = "biological-architecture/no-unregistered-plugin-dir";
@@ -1083,8 +1361,8 @@ var pluginDirsOf = (level) => {
1083
1361
  return { manifests: options.manifests ?? ["index.ts"], registry, roots: options.roots };
1084
1362
  };
1085
1363
  var pluginDirLayers = (root) => {
1086
- const path = join5(root, ".oxlintrc.json");
1087
- if (!existsSync4(path)) return [];
1364
+ const path = join7(root, ".oxlintrc.json");
1365
+ if (!existsSync6(path)) return [];
1088
1366
  const layers = [];
1089
1367
  for (const layer of layersOf(readConfig(path, root), PLUGIN_DIR_RULE)) {
1090
1368
  const dirs = pluginDirsOf(layer.level);
@@ -1093,7 +1371,7 @@ var pluginDirLayers = (root) => {
1093
1371
  return layers;
1094
1372
  };
1095
1373
  var layerOver = (layers, root, relative) => {
1096
- const at = join5(root, relative);
1374
+ const at = join7(root, relative);
1097
1375
  const inside = readdirSync3(at, { withFileTypes: true }).filter((entry) => entry.isFile()).map((entry) => `${relative}/${entry.name}`);
1098
1376
  const paths = inside.length === 0 ? [relative] : inside;
1099
1377
  const wrapped = layers.map((layer) => ({ files: layer.files, level: layer.dirs }));
@@ -1107,7 +1385,7 @@ var pluginDirs = (root) => {
1107
1385
  const layers = pluginDirLayers(root);
1108
1386
  if (layers.length === 0) {
1109
1387
  return [
1110
- finding3(
1388
+ finding4(
1111
1389
  "plugin directories",
1112
1390
  "SKIP",
1113
1391
  "no-unregistered-plugin-dir is not configured here, so this repo has not said where its integrations live"
@@ -1115,34 +1393,34 @@ var pluginDirs = (root) => {
1115
1393
  ];
1116
1394
  }
1117
1395
  const missing = [...new Set(layers.flatMap((layer) => layer.dirs.registry))].filter(
1118
- (registry) => !existsSync4(join5(root, registry))
1396
+ (registry) => !existsSync6(join7(root, registry))
1119
1397
  );
1120
1398
  if (missing.length > 0) {
1121
1399
  return missing.map(
1122
- (registry) => finding3(registry, "FAIL", "the registry the rule names is not there")
1400
+ (registry) => finding4(registry, "FAIL", "the registry the rule names is not there")
1123
1401
  );
1124
1402
  }
1125
1403
  const source = /* @__PURE__ */ new Map();
1126
1404
  const unreachable = /* @__PURE__ */ new Map();
1127
1405
  for (const rootDir of new Set(layers.flatMap((layer) => layer.dirs.roots))) {
1128
- const at = join5(root, rootDir);
1129
- if (!existsSync4(at)) continue;
1406
+ const at = join7(root, rootDir);
1407
+ if (!existsSync6(at)) continue;
1130
1408
  for (const entry of readdirSync3(at, { withFileTypes: true })) {
1131
1409
  if (!entry.isDirectory()) continue;
1132
1410
  const relative = `${rootDir}/${entry.name}`;
1133
1411
  const governs = layerOver(layers, root, relative);
1134
1412
  if (governs === void 0) continue;
1135
1413
  const key = governs.registry.join(", ");
1136
- const registry = source.get(key) ?? governs.registry.map((half) => readFileSync5(join5(root, half), "utf8")).join("\n");
1414
+ const registry = source.get(key) ?? governs.registry.map((half) => readFileSync7(join7(root, half), "utf8")).join("\n");
1137
1415
  source.set(key, registry);
1138
- const hasManifest = governs.manifests.some((name) => existsSync4(join5(at, entry.name, name)));
1416
+ const hasManifest = governs.manifests.some((name) => existsSync6(join7(at, entry.name, name)));
1139
1417
  if (!hasManifest || registry.includes(entry.name)) continue;
1140
1418
  unreachable.set(key, [...unreachable.get(key) ?? [], relative]);
1141
1419
  }
1142
1420
  }
1143
1421
  return [...source.keys()].toSorted().map((registry) => {
1144
1422
  const named2 = unreachable.get(registry) ?? [];
1145
- return named2.length === 0 ? finding3(registry, "OK", "every directory under the declared roots is named by it") : finding3(
1423
+ return named2.length === 0 ? finding4(registry, "OK", "every directory under the declared roots is named by it") : finding4(
1146
1424
  registry,
1147
1425
  "FAIL",
1148
1426
  `${named2.length} directory(ies) it never names: ${named2.join(", ")}`
@@ -1153,9 +1431,9 @@ var WORKSPACE_YAML = "pnpm-workspace.yaml";
1153
1431
  var HOIST_KEY = "publicHoistPattern";
1154
1432
  var HOIST_REPAIR = "rm -rf node_modules/.modules.yaml node_modules/.pnpm-workspace-state-v1.json && pnpm install";
1155
1433
  var hoistPatterns = (root) => {
1156
- const path = join5(root, WORKSPACE_YAML);
1157
- if (!existsSync4(path)) return void 0;
1158
- const lines = readFileSync5(path, "utf8").split("\n");
1434
+ const path = join7(root, WORKSPACE_YAML);
1435
+ if (!existsSync6(path)) return void 0;
1436
+ const lines = readFileSync7(path, "utf8").split("\n");
1159
1437
  const at = lines.findIndex((line) => new RegExp(`^${HOIST_KEY}\\s*:`).test(line));
1160
1438
  if (at < 0) return void 0;
1161
1439
  const patterns = [];
@@ -1172,7 +1450,7 @@ var publicHoists = (root, workspaces) => {
1172
1450
  const patterns = hoistPatterns(root);
1173
1451
  if (patterns === void 0) {
1174
1452
  return [
1175
- finding3(
1453
+ finding4(
1176
1454
  WORKSPACE_YAML,
1177
1455
  "SKIP",
1178
1456
  `no ${HOIST_KEY} is declared here, so nothing claims a package should be linked at the root`
@@ -1186,21 +1464,21 @@ var publicHoists = (root, workspaces) => {
1186
1464
  const shape = matching(pattern);
1187
1465
  const hoisted = named2.filter((name) => shape.test(name));
1188
1466
  if (hoisted.length === 0) {
1189
- return finding3(
1467
+ return finding4(
1190
1468
  pattern,
1191
1469
  "SKIP",
1192
1470
  `this ${HOIST_KEY} matches no workspace package here, so what it should have linked at the root is a question this cannot answer`
1193
1471
  );
1194
1472
  }
1195
- const pruned = hoisted.filter((name) => !existsSync4(join5(root, "node_modules", name)));
1473
+ const pruned = hoisted.filter((name) => !existsSync6(join7(root, "node_modules", name)));
1196
1474
  if (pruned.length === 0) {
1197
- return finding3(
1475
+ return finding4(
1198
1476
  pattern,
1199
1477
  "OK",
1200
1478
  `every workspace package this ${HOIST_KEY} matches is linked at the root`
1201
1479
  );
1202
1480
  }
1203
- return finding3(
1481
+ return finding4(
1204
1482
  pattern,
1205
1483
  "FAIL",
1206
1484
  `the ${HOIST_KEY} \`${pattern}\` has no root link for ${pruned.map((name) => `node_modules/${name}`).join(", ")} \u2014 pnpm prunes one during an unrelated add and then answers "Already up to date" over a clean git status forever. Repair: ${HOIST_REPAIR}`
@@ -1210,7 +1488,7 @@ var publicHoists = (root, workspaces) => {
1210
1488
  var checkDrift = ({
1211
1489
  readers = READERS,
1212
1490
  root,
1213
- userSettings = join5(homedir(), SETTINGS),
1491
+ userSettings = join7(homedir(), SETTINGS),
1214
1492
  workspaces
1215
1493
  }) => {
1216
1494
  const config = readGeonosis(root);
@@ -1236,21 +1514,21 @@ var checkDrift = ({
1236
1514
  import { spawnSync as spawnSync2 } from "child_process";
1237
1515
  import {
1238
1516
  cpSync,
1239
- existsSync as existsSync5,
1517
+ existsSync as existsSync7,
1240
1518
  mkdirSync,
1241
1519
  mkdtempSync,
1242
- readFileSync as readFileSync6,
1520
+ readFileSync as readFileSync8,
1243
1521
  rmSync,
1244
1522
  writeFileSync
1245
1523
  } from "fs";
1246
1524
  import { tmpdir } from "os";
1247
- import { dirname as dirname2, join as join6, resolve } from "path";
1525
+ import { dirname as dirname2, join as join8, resolve } from "path";
1248
1526
  import { corpusOf, readManifest } from "@geonosis/lint-parity";
1249
1527
  var OFF = /* @__PURE__ */ new Set([0, "0", "allow", "off", false]);
1250
1528
  var severityOf = (level) => Array.isArray(level) ? level[0] : level;
1251
1529
  var enabledRulesOf = (rules, plugin) => Object.keys(rules).filter((id) => id.startsWith(`${plugin}/`) && !OFF.has(severityOf(rules[id]))).toSorted();
1252
1530
  var passes = (findings) => !findings.some((one) => one.verdict === "FAIL" || one.verdict === "UNJUDGED");
1253
- var finding4 = (subject, verdict, message) => ({
1531
+ var finding5 = (subject, verdict, message) => ({
1254
1532
  check: "exercised",
1255
1533
  message,
1256
1534
  subject,
@@ -1262,9 +1540,9 @@ var refusal = (error) => {
1262
1540
  return said2.length > LIMIT ? `${said2.slice(0, LIMIT)}\u2026` : said2;
1263
1541
  };
1264
1542
  var reasonFrom = (config, oxlint) => {
1265
- const dir = mkdtempSync(join6(tmpdir(), "geonosis-doctor-why-"));
1543
+ const dir = mkdtempSync(join8(tmpdir(), "geonosis-doctor-why-"));
1266
1544
  try {
1267
- const probe = join6(dir, "probe.tsx");
1545
+ const probe = join8(dir, "probe.tsx");
1268
1546
  writeFileSync(probe, "export const probe = 1\n");
1269
1547
  const run = spawnSync2(
1270
1548
  oxlint,
@@ -1354,8 +1632,8 @@ var throughProbes = ({
1354
1632
  }) => {
1355
1633
  const refused = /* @__PURE__ */ new Map();
1356
1634
  const placed = /* @__PURE__ */ new Set();
1357
- const dir = mkdtempSync(join6(tmpdir(), "geonosis-doctor-probe-"));
1358
- const here = join6(dir, "corpus");
1635
+ const dir = mkdtempSync(join8(tmpdir(), "geonosis-doctor-probe-"));
1636
+ const here = join8(dir, "corpus");
1359
1637
  try {
1360
1638
  cpSync(corpus, here, { recursive: true });
1361
1639
  for (const rule of silent) {
@@ -1392,7 +1670,7 @@ var throughProbes = ({
1392
1670
  mounted = rehomed;
1393
1671
  }
1394
1672
  for (const file of mounted) {
1395
- const at2 = join6(here, file.path);
1673
+ const at2 = join8(here, file.path);
1396
1674
  if (placed.has(at2)) continue;
1397
1675
  placed.add(at2);
1398
1676
  mkdirSync(dirname2(at2), { recursive: true });
@@ -1405,12 +1683,12 @@ var throughProbes = ({
1405
1683
  refused.set(rule, String(error.message));
1406
1684
  }
1407
1685
  }
1408
- const probeConfig = JSON.parse(readFileSync6(config.path, "utf8"));
1686
+ const probeConfig = JSON.parse(readFileSync8(config.path, "utf8"));
1409
1687
  const configDir = dirname2(config.path);
1410
1688
  probeConfig.jsPlugins = (probeConfig.jsPlugins ?? []).map(
1411
1689
  (spec) => spec.startsWith(".") || spec.startsWith("/") ? resolve(configDir, spec) : packageDirOf(resolveFrom(configDir, spec), spec)
1412
1690
  );
1413
- const at = join6(here, ".oxlintrc-geonosis-probe.json");
1691
+ const at = join8(here, ".oxlintrc-geonosis-probe.json");
1414
1692
  writeFileSync(at, JSON.stringify(probeConfig, null, 2));
1415
1693
  const run = spawnSync2(
1416
1694
  oxlint,
@@ -1440,14 +1718,14 @@ var checkExercised = async ({
1440
1718
  repoCorpus,
1441
1719
  root
1442
1720
  }) => {
1443
- const said2 = (verdict, message) => finding4(config.relative, verdict, message);
1444
- if (repoCorpus !== void 0 && !existsSync5(repoCorpus)) {
1721
+ const said2 = (verdict, message) => finding5(config.relative, verdict, message);
1722
+ if (repoCorpus !== void 0 && !existsSync7(repoCorpus)) {
1445
1723
  return said2(
1446
1724
  "FAIL",
1447
1725
  `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`
1448
1726
  );
1449
1727
  }
1450
- if (!existsSync5(corpus)) {
1728
+ if (!existsSync7(corpus)) {
1451
1729
  return said2(
1452
1730
  "SKIP",
1453
1731
  `the plugin loaded from here ships no corpus at ${relativeToRoot(root, corpus)} \u2014 nothing declares which rules it can be evidence about`
@@ -1520,7 +1798,7 @@ var checkExercised = async ({
1520
1798
  );
1521
1799
  }
1522
1800
  const unplaceable = inert.map((rule) => `${rule}: ${probed.refused.get(rule) ?? ""}`);
1523
- const kitNote = unprobed.length === 0 || loadedProbes === null ? "" : `the loaded plugin ships no probe for ${unprobed.length === 1 ? "this enabled rule" : `${unprobed.length} enabled rules`} \u2014 the kit's omission, not this repo's (geonosis #131): ${unprobed.join(", ")}`;
1801
+ const kitNote = unprobed.length === 0 || loadedProbes === null ? "" : `the loaded plugin ships no probe for ${unprobed.length === 1 ? "this enabled rule" : `${unprobed.length} enabled rules`}, so the question cannot be asked here at all \u2014 the kit's omission, not this repo's, and nothing in this tree is the fix (geonosis #131): ${unprobed.join(", ")}`;
1524
1802
  const unknowable = loadedProbes === null ? unprobed.map(
1525
1803
  (rule) => `${rule} declares no probe, so ${optionSetsOf(config, rule).every((options) => options.length === 0) ? "its scope" : under(config, rule)} does not reach the corpus`
1526
1804
  ) : [];
@@ -1530,7 +1808,7 @@ var checkExercised = async ({
1530
1808
  named([...unknowable, ...unplaceable]) + (kitNote === "" ? "" : ` \u2014 and ${kitNote}`)
1531
1809
  );
1532
1810
  }
1533
- if (kitNote !== "") return said2("WARN", kitNote);
1811
+ if (kitNote !== "") return said2("SKIP", kitNote);
1534
1812
  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`;
1535
1813
  return said2(
1536
1814
  "OK",
@@ -1539,12 +1817,14 @@ var checkExercised = async ({
1539
1817
  };
1540
1818
 
1541
1819
  // src/envelope.ts
1542
- import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync7 } from "fs";
1543
- import { join as join7 } from "path";
1820
+ import { existsSync as existsSync8, readdirSync as readdirSync4, readFileSync as readFileSync9 } from "fs";
1821
+ import { join as join9 } from "path";
1544
1822
  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";
1823
+ var THIS_TOOL = "doctor";
1824
+ var OWN = `${THIS_TOOL}.json`;
1825
+ 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 (this run\u2019s own doctor.json is not one of them: it is written after these checks, and balanced at write time)";
1546
1826
  var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1547
- var finding5 = (verdict, subject, message) => ({
1827
+ var finding6 = (verdict, subject, message) => ({
1548
1828
  check: "envelope",
1549
1829
  message,
1550
1830
  subject,
@@ -1553,17 +1833,17 @@ var finding5 = (verdict, subject, message) => ({
1553
1833
  var lengthOf = (value) => Array.isArray(value) ? value.length : void 0;
1554
1834
  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
1835
  var judge = (subject, name, parsed) => {
1556
- if (!isRecord2(parsed)) return finding5("FAIL", subject, `is not an object. ${NEXT}`);
1836
+ if (!isRecord2(parsed)) return finding6("FAIL", subject, `is not an object. ${NEXT}`);
1557
1837
  const tool = parsed["tool"];
1558
1838
  if (typeof tool !== "string" || tool.trim() === "") {
1559
- return finding5(
1839
+ return finding6(
1560
1840
  "FAIL",
1561
1841
  subject,
1562
1842
  `names no tool, so nothing here says which run it is about. ${NEXT}`
1563
1843
  );
1564
1844
  }
1565
1845
  if (tool !== name) {
1566
- return finding5(
1846
+ return finding6(
1567
1847
  "FAIL",
1568
1848
  subject,
1569
1849
  `is written by "${tool}", not "${name}" \u2014 a tool writing another tool's envelope leaves both of their numbers unattributable. ${NEXT}`
@@ -1571,7 +1851,7 @@ var judge = (subject, name, parsed) => {
1571
1851
  }
1572
1852
  const version = parsed["version"];
1573
1853
  if (typeof version !== "string" || version.trim() === "") {
1574
- return finding5(
1854
+ return finding6(
1575
1855
  "FAIL",
1576
1856
  subject,
1577
1857
  `${tool}: names no version, so nothing dates this run and a stale envelope reads exactly like a fresh one. ${NEXT}`
@@ -1582,279 +1862,44 @@ var judge = (subject, name, parsed) => {
1582
1862
  const refused = lengthOf(parsed["refused"]);
1583
1863
  const excused = lengthOf(parsed["excused"]);
1584
1864
  if (typeof considered !== "number" || typeof read !== "number" || refused === void 0 || excused === void 0) {
1585
- return finding5(
1865
+ return finding6(
1586
1866
  "FAIL",
1587
1867
  subject,
1588
1868
  `${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
1869
  );
1590
1870
  }
1591
1871
  const accounted = read + refused + excused;
1592
- return considered === accounted ? finding5(
1872
+ return considered === accounted ? finding6(
1593
1873
  "OK",
1594
1874
  subject,
1595
1875
  `${tool} considered ${considered} and accounts for all of them \u2014 ${read} read + ${refused} refused + ${excused} excused`
1596
- ) : finding5(
1876
+ ) : finding6(
1597
1877
  "FAIL",
1598
1878
  subject,
1599
1879
  `${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
1880
  );
1601
1881
  };
1602
1882
  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)];
1883
+ const dir = join9(root, ENVELOPES_DIR);
1884
+ const files = existsSync8(dir) ? readdirSync4(dir).filter((name) => name.endsWith(".json") && name !== OWN).toSorted() : [];
1885
+ if (files.length === 0) return [finding6("SKIP", ENVELOPES_DIR, NO_ENVELOPES)];
1606
1886
  return files.map((name) => {
1607
1887
  const subject = `${ENVELOPES_DIR}/${name}`;
1608
1888
  let parsed;
1609
1889
  try {
1610
- parsed = JSON.parse(readFileSync7(join7(dir, name), "utf8"));
1890
+ parsed = JSON.parse(readFileSync9(join9(dir, name), "utf8"));
1611
1891
  } catch (error) {
1612
- return finding5("FAIL", subject, `is not readable JSON: ${error.message}. ${NEXT}`);
1892
+ return finding6("FAIL", subject, `is not readable JSON: ${error.message}. ${NEXT}`);
1613
1893
  }
1614
1894
  return judge(subject, name.replace(/\.json$/, ""), parsed);
1615
1895
  });
1616
1896
  };
1617
1897
 
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
1898
  // src/loaded.ts
1855
1899
  import { readFileSync as readFileSync10 } from "fs";
1856
1900
  import { join as join10, sep as sep3 } from "path";
1857
1901
  var SCOPE = "@geonosis/";
1902
+ var NOT_THE_SCOPE_DIRECTORY = "Deleting only the scope directory inside that node_modules is a repair no pnpm install undoes \u2014 the tree is then missing the package and every install stays green.";
1858
1903
  var BLOCKS = [
1859
1904
  "dependencies",
1860
1905
  "devDependencies",
@@ -1966,7 +2011,7 @@ var oneConfig = async ({
1966
2011
  }
1967
2012
  return held ? said2("OK", `loaded ${loaded.version} = declared ${declared.spec} (${declared.at})`) : said2(
1968
2013
  "FAIL",
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`
2014
+ `loaded ${loaded.version}, declared ${declared.spec} (${declared.at}) \u2014 a nested copy at ${loaded.at}. Run \`geonosis update --to ${declared.spec}\`: it removes the WHOLE node_modules of every non-root workspace carrying one, never the root's, and reinstalls. ${NOT_THE_SCOPE_DIRECTORY} The linter is not running what the tree declares until then`
1970
2015
  );
1971
2016
  };
1972
2017
  var labelOf2 = (workspace) => workspace.relative === "" ? "root" : workspace.relative;
@@ -2373,9 +2418,8 @@ var declaredIn3 = (manifest) => new Set(
2373
2418
  ].flatMap((block) => Object.keys(block ?? {}))
2374
2419
  );
2375
2420
  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
- );
2421
+ var labelOf3 = (workspace) => workspace.relative === "" ? "root" : workspace.relative;
2422
+ var governedBy = (dir, workspaces) => workspaces.filter((one) => holds3(one.dir, dir) || holds3(dir, one.dir));
2379
2423
  var enabledHere2 = (config, plugin) => [
2380
2424
  .../* @__PURE__ */ new Set([
2381
2425
  ...enabledRulesOf(config.rules, plugin),
@@ -2387,18 +2431,20 @@ var checkEngines = async ({
2387
2431
  entry,
2388
2432
  workspaces
2389
2433
  }) => {
2390
- const read = await presumptionsOf(entry).catch(() => void 0);
2391
- if (read === void 0 || read.namespace === "" || Object.keys(read.presumed).length === 0) {
2434
+ const plugin = await presumptionsOf(entry).catch(() => void 0);
2435
+ if (plugin === void 0 || plugin.namespace === "" || Object.keys(plugin.presumed).length === 0) {
2392
2436
  return [];
2393
2437
  }
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)];
2438
+ const governed = governedBy(config.dir, workspaces);
2439
+ const declared = new Set(governed.flatMap((one) => [...declaredIn3(one.manifest)]));
2440
+ const asked = governed.map(labelOf3).join(", ");
2441
+ return enabledHere2(config, plugin.namespace).flatMap((rule) => {
2442
+ const presumes = plugin.presumed[rule.slice(plugin.namespace.length + 1)];
2397
2443
  if (presumes === void 0 || presumes.packages.some((name) => declared.has(name))) return [];
2398
2444
  return [
2399
2445
  {
2400
2446
  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`,
2447
+ message: `it presumes ${presumes.engine}, and none of the manifests this config governs (${asked}) 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
2448
  subject: `${config.relative} \u2192 ${rule}`,
2403
2449
  verdict: "WARN"
2404
2450
  }
@@ -2552,6 +2598,15 @@ var missingOption = (error) => {
2552
2598
  const named2 = error.option;
2553
2599
  return typeof named2 === "string" ? named2 : HEADER.exec(String(error.message))?.[1];
2554
2600
  };
2601
+ var refusesBare = (rule) => {
2602
+ try {
2603
+ rule.create({ options: [], report: () => {
2604
+ } });
2605
+ return false;
2606
+ } catch (error) {
2607
+ return missingOption(error) !== void 0;
2608
+ }
2609
+ };
2555
2610
  var configured = (config, namespace) => [config.rules, ...config.overrides.map((one) => one.rules)].flatMap(
2556
2611
  (rules) => enabledRulesOf(rules, namespace).map((id) => [id, optionsOf2(rules[id])])
2557
2612
  );
@@ -2563,26 +2618,37 @@ var checkRequiredOptions = async ({
2563
2618
  const namespace = loaded?.default?.meta?.name;
2564
2619
  const rules = loaded?.default?.rules;
2565
2620
  if (typeof namespace !== "string" || namespace === "" || rules === void 0) return [];
2566
- return configured(config, namespace).flatMap(([id, options]) => {
2621
+ const enabled = configured(config, namespace);
2622
+ if (enabled.length === 0) return [];
2623
+ const missing = [];
2624
+ let gated = 0;
2625
+ for (const [id, options] of enabled) {
2567
2626
  const rule = rules[id.slice(namespace.length + 1)];
2568
- if (rule === void 0) return [];
2627
+ if (rule === void 0) continue;
2628
+ if (refusesBare(rule)) gated += 1;
2569
2629
  try {
2570
2630
  rule.create({ options, report: () => {
2571
2631
  } });
2572
- return [];
2573
2632
  } catch (error) {
2574
2633
  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
- ];
2634
+ if (option === void 0) continue;
2635
+ missing.push({
2636
+ check: "loaded",
2637
+ 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`,
2638
+ subject: `${config.relative} \u2192 ${id}`,
2639
+ verdict: "FAIL"
2640
+ });
2584
2641
  }
2585
- });
2642
+ }
2643
+ if (missing.length > 0) return missing;
2644
+ return [
2645
+ {
2646
+ check: "loaded",
2647
+ message: `${gated} rules require options, ${gated} carry them`,
2648
+ subject: config.relative,
2649
+ verdict: "OK"
2650
+ }
2651
+ ];
2586
2652
  };
2587
2653
 
2588
2654
  // src/doctor.ts
@@ -2611,7 +2677,7 @@ var exercisedOf = ({
2611
2677
  ];
2612
2678
  }
2613
2679
  const own = repoCorpus !== void 0 && config.relative === CONFIG_FILE ? repoCorpus : void 0;
2614
- const [reach, engines, options] = await Promise.all([
2680
+ const [reach, engines] = await Promise.all([
2615
2681
  checkExercised({
2616
2682
  config,
2617
2683
  corpus,
@@ -2622,15 +2688,28 @@ var exercisedOf = ({
2622
2688
  }),
2623
2689
  // #159: the rules whose engine this tree has not got. Beside the reach line rather than
2624
2690
  // 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 })
2691
+ checkEngines({ config, entry, workspaces })
2629
2692
  ]);
2630
- return [reach, ...engines, ...options];
2693
+ return [reach, ...engines];
2631
2694
  })
2632
2695
  )
2633
2696
  );
2697
+ var requiredOptionsOf = async (configs) => {
2698
+ const found = await Promise.all(
2699
+ configs.flatMap(
2700
+ (config) => config.jsPlugins.filter((name) => name.startsWith(SCOPE)).map(async (specifier) => {
2701
+ let entry;
2702
+ try {
2703
+ entry = resolveFrom(config.dir, specifier);
2704
+ } catch {
2705
+ return [];
2706
+ }
2707
+ return checkRequiredOptions({ config, entry });
2708
+ })
2709
+ )
2710
+ );
2711
+ return found.flat();
2712
+ };
2634
2713
  var skip = (message) => [
2635
2714
  { check: "baseline", message, subject: RATCHET_FILE, verdict: "SKIP" }
2636
2715
  ];
@@ -2668,7 +2747,13 @@ var runDoctor = async ({
2668
2747
  }
2669
2748
  const asked = (check) => only === void 0 || only.includes(check);
2670
2749
  const runs = [
2671
- ["loaded", () => checkLoaded({ configs, root, workspaces })],
2750
+ [
2751
+ "loaded",
2752
+ async () => [
2753
+ ...await checkLoaded({ configs, root, workspaces }),
2754
+ ...await requiredOptionsOf(configs)
2755
+ ]
2756
+ ],
2672
2757
  [
2673
2758
  "group",
2674
2759
  () => {
@@ -2702,13 +2787,19 @@ var runDoctor = async ({
2702
2787
  ["rails", () => checkRails({ root, userSettings: join14(homedir2(), USER_SETTINGS) })]
2703
2788
  ];
2704
2789
  const collected = [];
2705
- for (const [check, run] of runs) if (asked(check)) collected.push(...await run());
2790
+ const ran = [];
2791
+ for (const [check, run] of runs) {
2792
+ if (!asked(check)) continue;
2793
+ ran.push(check);
2794
+ collected.push(...await run());
2795
+ }
2706
2796
  const found = ordered(collected);
2707
2797
  const findings = strict ? found.map((one) => one.verdict === "WARN" ? { ...one, verdict: "FAIL" } : one) : found;
2708
2798
  return {
2709
2799
  counts: countsOf(findings),
2710
2800
  findings,
2711
2801
  ok: passes(findings),
2802
+ ran,
2712
2803
  root
2713
2804
  };
2714
2805
  };
@@ -2765,6 +2856,8 @@ export {
2765
2856
  DEPLOYED_FILE,
2766
2857
  NOT_WRITTEN,
2767
2858
  checkDeployed,
2859
+ GEONOSIS_FILE,
2860
+ repoCorpusOf,
2768
2861
  CHECKS,
2769
2862
  DoctorError,
2770
2863
  resolveFrom,
@@ -2772,6 +2865,10 @@ export {
2772
2865
  pluginVersionOf,
2773
2866
  corpusOfPlugin,
2774
2867
  relativeToRoot,
2868
+ FIXED_GROUP,
2869
+ KIT_GROUP,
2870
+ declaredGroupsOf,
2871
+ checkGroup,
2775
2872
  COMPOSITION_ROOT,
2776
2873
  FLOOR_PACKAGES,
2777
2874
  READERS,
@@ -2781,12 +2878,6 @@ export {
2781
2878
  ENVELOPES_DIR,
2782
2879
  NO_ENVELOPES,
2783
2880
  checkEnvelopes,
2784
- GEONOSIS_FILE,
2785
- repoCorpusOf,
2786
- FIXED_GROUP,
2787
- KIT_GROUP,
2788
- declaredGroupsOf,
2789
- checkGroup,
2790
2881
  SCOPE,
2791
2882
  satisfies,
2792
2883
  declaredFor,