@geonosis/doctor 2.0.0 → 2.2.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
  }
@@ -411,6 +427,14 @@ var presumptionsOf = async (entry) => {
411
427
  const loaded = await import(pathToFileURL(entry).href);
412
428
  const namespace = loaded.default?.meta?.name;
413
429
  return {
430
+ // #290: a rule that names a pattern whose floor is not shipped is a requirement with nothing to
431
+ // install behind it — the second consumer hand-writes the floor to satisfy the lint.
432
+ floors: Object.fromEntries(
433
+ Object.entries(loaded.default?.rules ?? {}).flatMap(([name, rule]) => {
434
+ const floor = rule?.meta?.floor;
435
+ return typeof floor?.name === "string" && floor.pending === true ? [[name, { name: floor.name }]] : [];
436
+ })
437
+ ),
414
438
  namespace: typeof namespace === "string" ? namespace : "",
415
439
  presumed: Object.fromEntries(
416
440
  Object.entries(loaded.default?.rules ?? {}).flatMap(([name, rule]) => {
@@ -421,7 +445,7 @@ var presumptionsOf = async (entry) => {
421
445
  )
422
446
  };
423
447
  };
424
- var corpusOfPlugin = (from, specifier) => join3(packageDirOf(resolveFrom(from, specifier), specifier), "corpus");
448
+ var corpusOfPlugin = (from, specifier) => join4(packageDirOf(resolveFrom(from, specifier), specifier), "corpus");
425
449
  var real = (path) => {
426
450
  try {
427
451
  return realpathSync(path);
@@ -431,14 +455,13 @@ var real = (path) => {
431
455
  };
432
456
  var relativeToRoot = (root, path) => relativePath(real(root), real(path));
433
457
 
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";
458
+ // src/group.ts
459
+ import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
460
+ import { join as join6 } from "path";
438
461
 
439
462
  // src/hooks.ts
440
- import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
441
- import { join as join4 } from "path";
463
+ import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync5 } from "fs";
464
+ import { join as join5 } from "path";
442
465
  var HOOK_FILES = ["lefthook.yml", "lefthook.yaml", ".lefthook.yml", ".lefthook.yaml"];
443
466
  var HOOK_DIRS = [".husky", ".githooks"];
444
467
  var VERSION_MANAGED = /(?:^|[\s;&|(])(?<runner>pnpm|npx|bunx|yarn)\b(?:\s+(?:exec|run|dlx|x))?\s+(?<bin>geonosis(?:-[a-z-]+)?)\b/;
@@ -447,26 +470,267 @@ var hookLines = (root) => {
447
470
  const found = [];
448
471
  const read = (path) => {
449
472
  const at = relativePath(root, path);
450
- for (const line of readFileSync4(path, "utf8").split("\n")) {
473
+ for (const line of readFileSync5(path, "utf8").split("\n")) {
451
474
  if (line.trim() !== "") found.push({ at, line });
452
475
  }
453
476
  };
454
477
  for (const name of HOOK_FILES) {
455
- const path = join4(root, name);
456
- if (existsSync3(path)) read(path);
478
+ const path = join5(root, name);
479
+ if (existsSync4(path)) read(path);
457
480
  }
458
481
  for (const name of HOOK_DIRS) {
459
- const dir = join4(root, name);
460
- if (!existsSync3(dir)) continue;
482
+ const dir = join5(root, name);
483
+ if (!existsSync4(dir)) continue;
461
484
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
462
485
  if (entry.isFile() && !entry.name.startsWith("_") && !entry.name.startsWith(".")) {
463
- read(join4(dir, entry.name));
486
+ read(join5(dir, entry.name));
464
487
  }
465
488
  }
466
489
  }
467
490
  return found;
468
491
  };
469
492
 
493
+ // src/group.ts
494
+ var FIXED_GROUP = [
495
+ "@geonosis/cli",
496
+ "@geonosis/doctor",
497
+ "@geonosis/evals",
498
+ "@geonosis/integrations",
499
+ "@geonosis/ledger",
500
+ "@geonosis/lint-parity",
501
+ "@geonosis/mcp",
502
+ "@geonosis/observability",
503
+ "@geonosis/oxlint-plugin-biological-architecture",
504
+ "@geonosis/policy",
505
+ "@geonosis/rails",
506
+ "@geonosis/ratchet",
507
+ "@geonosis/release",
508
+ "@geonosis/review",
509
+ "@geonosis/testbed",
510
+ "@geonosis/themekit",
511
+ "@geonosis/verify",
512
+ "@geonosis/verify-arch",
513
+ "@geonosis/visual-diff",
514
+ "@geonosis/walk",
515
+ "create-geonosis",
516
+ "geonosis"
517
+ ];
518
+ var KIT_GROUP = { name: "@geonosis fixed group", packages: FIXED_GROUP };
519
+ var declaredGroupsOf = (root) => {
520
+ const path = join6(root, GEONOSIS_FILE);
521
+ if (!existsSync5(path)) return void 0;
522
+ let parsed;
523
+ try {
524
+ parsed = JSON.parse(readFileSync6(path, "utf8"));
525
+ } catch {
526
+ return void 0;
527
+ }
528
+ const declared = parsed.doctor?.groups;
529
+ if (declared === void 0) return void 0;
530
+ if (!Array.isArray(declared) || declared.length === 0) {
531
+ throw new DoctorError(
532
+ `${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`
533
+ );
534
+ }
535
+ return declared.map((one, at) => {
536
+ const group = one;
537
+ if (typeof group.name !== "string" || !Array.isArray(group.packages) || group.packages.length < 2) {
538
+ throw new DoctorError(
539
+ `${GEONOSIS_FILE} doctor.groups[${at}] needs a name and at least two packages \u2014 one package cannot disagree with itself`
540
+ );
541
+ }
542
+ return { name: group.name, packages: group.packages };
543
+ });
544
+ };
545
+ var labelOf = (workspace) => workspace.relative === "" ? "root" : workspace.relative;
546
+ var versionOf = (dir, name) => {
547
+ let entry;
548
+ try {
549
+ entry = resolveFrom(dir, name);
550
+ } catch {
551
+ return { kind: "absent" };
552
+ }
553
+ try {
554
+ const manifest = JSON.parse(
555
+ readFileSync6(join6(packageDirOf(entry, name), "package.json"), "utf8")
556
+ );
557
+ return typeof manifest.version === "string" ? { kind: "read", version: manifest.version } : { kind: "unreadable", why: "its package.json declares no version" };
558
+ } catch (error) {
559
+ return { kind: "unreadable", why: String(error.message).split("\n")[0] ?? "" };
560
+ }
561
+ };
562
+ var finding3 = (subject, verdict, message) => ({
563
+ check: "group",
564
+ message,
565
+ subject,
566
+ verdict
567
+ });
568
+ var said = (versions) => [...versions.entries()].map(([version, from]) => `${version} (${from.join(", ")})`).join("; ");
569
+ var repoWide = (name, found) => {
570
+ const versions = /* @__PURE__ */ new Map();
571
+ for (const one of found) versions.set(one.version, [...versions.get(one.version) ?? [], one.at]);
572
+ if (versions.size === 1) return void 0;
573
+ return finding3(
574
+ name,
575
+ "FAIL",
576
+ `${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`
577
+ );
578
+ };
579
+ var perWorkspace = (group, at, found) => {
580
+ if (new Set(found.map((one) => one.version)).size === 1) return void 0;
581
+ const listed = found.map((one) => `${one.name} ${one.version}`).join(", ");
582
+ return finding3(
583
+ at,
584
+ "FAIL",
585
+ `${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`
586
+ );
587
+ };
588
+ var DOORS = ["@geonosis/cli", "geonosis"];
589
+ var manifestOf = (dir, name) => {
590
+ try {
591
+ return JSON.parse(
592
+ readFileSync6(join6(packageDirOf(resolveFrom(dir, name), name), "package.json"), "utf8")
593
+ );
594
+ } catch {
595
+ return void 0;
596
+ }
597
+ };
598
+ var binNamesOf = (manifest, name) => {
599
+ const declared = manifest?.bin;
600
+ return typeof declared === "string" ? [name.replace(/^@[^/]+\//, "")] : Object.keys(declared ?? {});
601
+ };
602
+ var declaredIn2 = (manifest) => [
603
+ ...new Set(
604
+ [
605
+ manifest.dependencies,
606
+ manifest.devDependencies,
607
+ manifest.optionalDependencies,
608
+ manifest.peerDependencies
609
+ ].flatMap((block) => Object.keys(block ?? {}))
610
+ )
611
+ ];
612
+ var calls = (lines, bin) => new RegExp(`(?:^|[\\s;&|(/])${bin}(?:\\s|$)`).test(lines);
613
+ var workspaceCaller = (workspaces, bins) => {
614
+ for (const workspace of workspaces) {
615
+ if (workspace.relative === "") continue;
616
+ for (const [script, line] of Object.entries(workspace.manifest.scripts ?? {})) {
617
+ if (bins.some((bin) => calls(line, bin))) return { at: workspace.relative, script };
618
+ }
619
+ }
620
+ return void 0;
621
+ };
622
+ var rootDeclarations = (root, workspaces) => {
623
+ const here = workspaces.find((one) => one.relative === "");
624
+ if (here === void 0) return [];
625
+ const declared = declaredIn2(here.manifest);
626
+ const door = DOORS.find((name) => declared.includes(name));
627
+ if (door === void 0) return [];
628
+ const owned = new Set(
629
+ [door, ...DOORS].flatMap((name) => {
630
+ const manifest = manifestOf(root, name);
631
+ return manifest === void 0 ? [] : Object.keys(manifest.dependencies ?? {});
632
+ })
633
+ );
634
+ const callers = [
635
+ ...Object.values(here.manifest.scripts ?? {}),
636
+ ...hookLines(root).map((one) => one.line)
637
+ ].join("\n");
638
+ const loadedHere = new Set(readConfig(join6(root, CONFIG_FILE), root).jsPlugins);
639
+ const spare = declared.filter(
640
+ (name) => !DOORS.includes(name) && owned.has(name) && !loadedHere.has(name) && !binNamesOf(manifestOf(root, name), name).some((bin) => calls(callers, bin))
641
+ );
642
+ if (spare.length === 0) {
643
+ return [
644
+ finding3(
645
+ "package.json",
646
+ "OK",
647
+ `every @geonosis package this root declares is ${door} or a bin its own scripts call by name`
648
+ )
649
+ ];
650
+ }
651
+ return spare.map((name) => {
652
+ const bins = binNamesOf(manifestOf(root, name), name);
653
+ const inWorkspace = workspaceCaller(workspaces, bins);
654
+ if (inWorkspace !== void 0) {
655
+ return finding3(
656
+ name,
657
+ "OK",
658
+ `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`
659
+ );
660
+ }
661
+ return finding3(
662
+ name,
663
+ "WARN",
664
+ `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`
665
+ );
666
+ });
667
+ };
668
+ var checkGroup = ({
669
+ groups,
670
+ root,
671
+ workspaces
672
+ }) => {
673
+ const findings = [...rootDeclarations(root, workspaces)];
674
+ for (const group of groups ?? [KIT_GROUP]) {
675
+ const found = [];
676
+ for (const workspace of workspaces) {
677
+ const at = labelOf(workspace);
678
+ const here = [];
679
+ for (const name of group.packages) {
680
+ const reading = versionOf(workspace.dir, name);
681
+ if (reading.kind === "read") here.push({ at, name, version: reading.version });
682
+ if (reading.kind === "unreadable") {
683
+ findings.push(
684
+ finding3(
685
+ `${at} \u2192 ${name}`,
686
+ "UNJUDGED",
687
+ `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`
688
+ )
689
+ );
690
+ }
691
+ }
692
+ const split2 = here.length > 0 ? perWorkspace(group, at, here) : void 0;
693
+ if (split2 !== void 0) findings.push(split2);
694
+ found.push(...here);
695
+ }
696
+ if (found.length === 0) {
697
+ findings.push(
698
+ finding3(
699
+ group.name,
700
+ "SKIP",
701
+ `no workspace under ${root} resolves any of ${group.packages.join(", ")}, so nothing here versions together`
702
+ )
703
+ );
704
+ continue;
705
+ }
706
+ const names = [...new Set(found.map((one) => one.name))].toSorted();
707
+ const split = names.flatMap((name) => {
708
+ const line = repoWide(
709
+ name,
710
+ found.filter((one) => one.name === name)
711
+ );
712
+ return line === void 0 ? [] : [line];
713
+ });
714
+ findings.push(...split);
715
+ if (split.length === 0) {
716
+ const versions = [...new Set(found.map((one) => one.version))];
717
+ findings.push(
718
+ finding3(
719
+ group.name,
720
+ "OK",
721
+ `${names.length} of ${group.packages.length} installed, at ${versions.join(", ")}, one copy each across ${new Set(found.map((one) => one.at)).size} workspaces`
722
+ )
723
+ );
724
+ }
725
+ }
726
+ return findings;
727
+ };
728
+
729
+ // src/drift.ts
730
+ import { closeSync, existsSync as existsSync6, openSync, readdirSync as readdirSync3, readFileSync as readFileSync7, readSync } from "fs";
731
+ import { homedir } from "os";
732
+ import { join as join7, sep as sep2 } from "path";
733
+
470
734
  // src/overrides.ts
471
735
  var SPECIAL = /* @__PURE__ */ new Set(["$", "(", ")", "+", ".", "/", "@", "\\", "^", "|"]);
472
736
  var globToRegExp = (glob) => {
@@ -537,7 +801,7 @@ var GEONOSIS = "geonosis.json";
537
801
  var LAW = "CLAUDE.md";
538
802
  var CEILING = 200;
539
803
  var SWITCHED_OFF = /^\s*if:\s*(?:\$\{\{\s*)?false\b/m;
540
- var finding3 = (subject, verdict, message) => ({
804
+ var finding4 = (subject, verdict, message) => ({
541
805
  check: "drift",
542
806
  message,
543
807
  subject,
@@ -555,27 +819,27 @@ var filesUnder = (dir, match) => {
555
819
  }
556
820
  for (const entry of entries) {
557
821
  if (entry.isDirectory()) {
558
- if (!entry.name.startsWith(".") && !NEVER_WALKED2.has(entry.name)) walk2(join5(at, entry.name));
822
+ if (!entry.name.startsWith(".") && !NEVER_WALKED2.has(entry.name)) walk2(join7(at, entry.name));
559
823
  continue;
560
824
  }
561
- if (entry.isFile() && match(entry.name)) found.push(join5(at, entry.name));
825
+ if (entry.isFile() && match(entry.name)) found.push(join7(at, entry.name));
562
826
  }
563
827
  };
564
828
  walk2(dir);
565
829
  return found;
566
830
  };
567
831
  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")];
832
+ const dir = join7(root, WORKFLOWS);
833
+ if (!existsSync6(dir)) {
834
+ return [finding4(WORKFLOWS, "SKIP", "there are no workflows here to read")];
571
835
  }
572
836
  return filesUnder(dir, (name) => name.endsWith(".yml") || name.endsWith(".yaml")).map((path) => {
573
837
  const at = relativePath(root, path);
574
- return SWITCHED_OFF.test(readFileSync5(path, "utf8")) ? finding3(
838
+ return SWITCHED_OFF.test(readFileSync7(path, "utf8")) ? finding4(
575
839
  at,
576
840
  "FAIL",
577
841
  "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");
842
+ ) : finding4(at, "OK", "nothing in it is switched off");
579
843
  });
580
844
  };
581
845
  var holds = (parent, child) => child === parent || child.startsWith(`${parent}${sep2}`);
@@ -591,10 +855,10 @@ var orphanTests = (root, workspaces) => {
591
855
  orphaned.set(at, [...orphaned.get(at) ?? [], relativePath(root, path)]);
592
856
  }
593
857
  if (orphaned.size === 0) {
594
- return [finding3("test files", "OK", "every test file sits under a workspace that runs tests")];
858
+ return [finding4("test files", "OK", "every test file sits under a workspace that runs tests")];
595
859
  }
596
860
  return [...orphaned.entries()].map(
597
- ([at, files]) => finding3(
861
+ ([at, files]) => finding4(
598
862
  at,
599
863
  "FAIL",
600
864
  `${files.length} test file(s) here and no test script to run them \u2014 ${files.slice(0, 3).join(", ")}`
@@ -619,16 +883,16 @@ var pathsRunByScript = (script) => script.split(BETWEEN_COMMANDS).map(pathRunBy)
619
883
  var scriptPaths = (workspaces) => {
620
884
  const missing = workspaces.flatMap(
621
885
  (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 }))
886
+ ([name, script]) => pathsRunByScript(script).filter((path) => !existsSync6(join7(one.dir, path))).map((path) => ({ name, path, workspace: one }))
623
887
  )
624
888
  );
625
889
  if (missing.length === 0) {
626
890
  return [
627
- finding3("script paths", "OK", "every file a script hands to bun, node or tsx is on disk")
891
+ finding4("script paths", "OK", "every file a script hands to bun, node or tsx is on disk")
628
892
  ];
629
893
  }
630
894
  return missing.map(
631
- ({ name, path, workspace }) => finding3(
895
+ ({ name, path, workspace }) => finding4(
632
896
  workspace.relative === "" ? "package.json" : `${workspace.relative}/package.json`,
633
897
  "FAIL",
634
898
  `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 +915,16 @@ var workspaceBins = (workspaces) => {
651
915
  var linkedBins = (root, workspaces) => {
652
916
  const bins = workspaceBins(workspaces);
653
917
  if (bins.size === 0) {
654
- return [finding3("workspace bins", "SKIP", "no workspace here declares a bin")];
918
+ return [finding4("workspace bins", "SKIP", "no workspace here declares a bin")];
655
919
  }
656
920
  const missing = workspaces.flatMap(
657
921
  (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 }))
922
+ ([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
923
  )
660
924
  );
661
925
  if (missing.length === 0) {
662
926
  return [
663
- finding3(
927
+ finding4(
664
928
  "workspace bins",
665
929
  "OK",
666
930
  "every workspace bin a script names is linked under the root"
@@ -668,7 +932,7 @@ var linkedBins = (root, workspaces) => {
668
932
  ];
669
933
  }
670
934
  return missing.map(
671
- ({ name, script, workspace }) => finding3(
935
+ ({ name, script, workspace }) => finding4(
672
936
  name,
673
937
  "FAIL",
674
938
  `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 +954,7 @@ var preloadAroundPnpm = (root, workspaces) => {
690
954
  const some = scripts.slice(0, 3).join(", ");
691
955
  const rest = scripts.length > 3 ? ` and ${scripts.length - 3} more` : "";
692
956
  return [
693
- finding3(
957
+ finding4(
694
958
  "NODE_OPTIONS",
695
959
  "WARN",
696
960
  `${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 +990,11 @@ var everythingRun = (root, workspaces) => {
726
990
  const scripts = workspaces.flatMap((one) => Object.values(one.manifest.scripts ?? {}));
727
991
  const counters = (readRatchet(root)?.counters ?? []).map((entry) => String(entry.command ?? ""));
728
992
  const workflows = filesUnder(
729
- join5(root, WORKFLOWS),
993
+ join7(root, WORKFLOWS),
730
994
  (name) => name.endsWith(".yml") || name.endsWith(".yaml")
731
995
  ).map((path) => {
732
996
  try {
733
- return readFileSync5(path, "utf8");
997
+ return readFileSync7(path, "utf8");
734
998
  } catch {
735
999
  return "";
736
1000
  }
@@ -741,7 +1005,7 @@ var generatedFiles = (root, workspaces) => {
741
1005
  const marked = filesUnder(root, (name) => CAN_CARRY_A_MARKER.test(name)).map((path) => ({ marker: markerIn(path), path })).filter((one) => one.marker !== void 0);
742
1006
  if (marked.length === 0) {
743
1007
  return [
744
- finding3(
1008
+ finding4(
745
1009
  "generated files",
746
1010
  "SKIP",
747
1011
  "nothing here says it was generated, so there is no write half here to look for a read half of"
@@ -752,13 +1016,13 @@ var generatedFiles = (root, workspaces) => {
752
1016
  return marked.map(({ marker, path }) => {
753
1017
  const at = relativePath(root, path);
754
1018
  if (!("gate" in marker)) {
755
- return finding3(
1019
+ return finding4(
756
1020
  at,
757
1021
  "FAIL",
758
1022
  `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
1023
  );
760
1024
  }
761
- return run.includes(marker.gate) ? finding3(at, "OK", `generated, and \`${marker.gate}\` reads it back`) : finding3(
1025
+ return run.includes(marker.gate) ? finding4(at, "OK", `generated, and \`${marker.gate}\` reads it back`) : finding4(
762
1026
  at,
763
1027
  "FAIL",
764
1028
  `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 +1030,10 @@ var generatedFiles = (root, workspaces) => {
766
1030
  });
767
1031
  };
768
1032
  var readGeonosis = (root) => {
769
- const path = join5(root, GEONOSIS);
770
- if (!existsSync4(path)) return void 0;
1033
+ const path = join7(root, GEONOSIS);
1034
+ if (!existsSync6(path)) return void 0;
771
1035
  try {
772
- return JSON.parse(readFileSync5(path, "utf8"));
1036
+ return JSON.parse(readFileSync7(path, "utf8"));
773
1037
  } catch {
774
1038
  return void 0;
775
1039
  }
@@ -777,15 +1041,15 @@ var readGeonosis = (root) => {
777
1041
  var law = (root, config) => {
778
1042
  const declared = config?.law ?? {};
779
1043
  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")];
1044
+ const path = join7(root, file);
1045
+ if (!existsSync6(path)) {
1046
+ return [finding4(file, "SKIP", "there is no law file here to measure")];
783
1047
  }
784
- const source = readFileSync5(path, "utf8");
1048
+ const source = readFileSync7(path, "utf8");
785
1049
  const lines = source.split("\n").length - (source.endsWith("\n") ? 1 : 0);
786
1050
  if (typeof declared.maxLines !== "number") {
787
1051
  return [
788
- finding3(
1052
+ finding4(
789
1053
  file,
790
1054
  "SKIP",
791
1055
  `${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 +1058,18 @@ var law = (root, config) => {
794
1058
  }
795
1059
  const ceiling = declared.maxLines;
796
1060
  return [
797
- lines > ceiling ? finding3(
1061
+ lines > ceiling ? finding4(
798
1062
  file,
799
1063
  "WARN",
800
1064
  `${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}`)
1065
+ ) : finding4(file, "OK", `${lines} lines, under the ceiling of ${ceiling}`)
802
1066
  ];
803
1067
  };
804
1068
  var KIT_PLUGIN = "geonosis";
805
1069
  var enablesKit = (path) => {
806
- if (!existsSync4(path)) return false;
1070
+ if (!existsSync6(path)) return false;
807
1071
  try {
808
- const parsed = JSON.parse(readFileSync5(path, "utf8"));
1072
+ const parsed = JSON.parse(readFileSync7(path, "utf8"));
809
1073
  const enabled = parsed.enabledPlugins;
810
1074
  if (typeof enabled !== "object" || enabled === null) return false;
811
1075
  return Object.entries(enabled).some(
@@ -816,13 +1080,13 @@ var enablesKit = (path) => {
816
1080
  }
817
1081
  };
818
1082
  var hooks = (root, userSettings) => {
819
- const path = join5(root, SETTINGS);
1083
+ const path = join7(root, SETTINGS);
820
1084
  if (enablesKit(path)) {
821
- return [finding3(SETTINGS, "OK", `\`enabledPlugins\` here enables the kit\u2019s plugin`)];
1085
+ return [finding4(SETTINGS, "OK", `\`enabledPlugins\` here enables the kit\u2019s plugin`)];
822
1086
  }
823
1087
  if (enablesKit(userSettings)) {
824
1088
  return [
825
- finding3(
1089
+ finding4(
826
1090
  SETTINGS,
827
1091
  "OK",
828
1092
  `installed elsewhere \u2014 not this repo\u2019s to declare: \`enabledPlugins\` in ${userSettings} enables it for every repo on this machine`
@@ -830,7 +1094,7 @@ var hooks = (root, userSettings) => {
830
1094
  ];
831
1095
  }
832
1096
  return [
833
- finding3(
1097
+ finding4(
834
1098
  SETTINGS,
835
1099
  "WARN",
836
1100
  `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 +1103,9 @@ var hooks = (root, userSettings) => {
839
1103
  };
840
1104
  var ALLOW_BUILDS_KEY = "allowBuilds";
841
1105
  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");
1106
+ const path = join7(root, WORKSPACE_YAML);
1107
+ if (!existsSync6(path)) return void 0;
1108
+ const lines = readFileSync7(path, "utf8").split("\n");
845
1109
  const at = lines.findIndex((line) => new RegExp(`^${ALLOW_BUILDS_KEY}\\s*:`).test(line));
846
1110
  if (at < 0) return void 0;
847
1111
  const names = [];
@@ -866,7 +1130,7 @@ var platformSplitBuilds = (root) => {
866
1130
  }
867
1131
  return [...families.entries()].flatMap(
868
1132
  ([family, allowed]) => allowed.length > 1 ? [] : [
869
- finding3(
1133
+ finding4(
870
1134
  allowed[0] ?? family,
871
1135
  "WARN",
872
1136
  `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 +1156,7 @@ var pathsLintedBy = (script, dir) => {
892
1156
  if (word.startsWith("-")) continue;
893
1157
  const before = words[at - 1] ?? "";
894
1158
  if (TAKES_A_VALUE.has(before)) continue;
895
- if (existsSync4(join5(dir, word))) named2.push(word.replace(/^\.\//, "").replace(/\/$/, ""));
1159
+ if (existsSync6(join7(dir, word))) named2.push(word.replace(/^\.\//, "").replace(/\/$/, ""));
896
1160
  }
897
1161
  return named2;
898
1162
  };
@@ -905,13 +1169,13 @@ var sourceDirsIn = (dir) => {
905
1169
  return [];
906
1170
  }
907
1171
  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
1172
+ (entry) => entry.isDirectory() && !entry.name.startsWith(".") && !NEVER_WALKED2.has(entry.name) && filesUnder(join7(dir, entry.name), (name) => LINTABLE.test(name)).length > 0
909
1173
  ).map((entry) => entry.name);
910
1174
  };
911
1175
  var lintScopes = (root, workspaces) => {
912
1176
  if (!hookLines(root).some((one) => LINTS_STAGED.test(one.line))) {
913
1177
  return [
914
- finding3(
1178
+ finding4(
915
1179
  "lint scope",
916
1180
  "SKIP",
917
1181
  "no committed hook lints staged files here, so there is no second file set for a lint script to be narrower than"
@@ -924,16 +1188,16 @@ var lintScopes = (root, workspaces) => {
924
1188
  const at = workspace.relative === "" ? "package.json" : `${workspace.relative}/package.json`;
925
1189
  const covered = pathsLintedBy(script, workspace.dir);
926
1190
  if (covered.length === 0 || covered.includes(".")) {
927
- return [finding3(at, "OK", `"${script}" covers everything the hook could stage here`)];
1191
+ return [finding4(at, "OK", `"${script}" covers everything the hook could stage here`)];
928
1192
  }
929
1193
  const uncovered = sourceDirsIn(workspace.dir).filter(
930
1194
  (name) => !covered.some((path) => name === path || path.startsWith(`${name}/`))
931
1195
  );
932
1196
  if (uncovered.length === 0) {
933
- return [finding3(at, "OK", `"${script}" covers every source directory under this workspace`)];
1197
+ return [finding4(at, "OK", `"${script}" covers every source directory under this workspace`)];
934
1198
  }
935
1199
  return [
936
- finding3(
1200
+ finding4(
937
1201
  at,
938
1202
  "WARN",
939
1203
  `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`
@@ -961,6 +1225,10 @@ var COMPOSABLE = [
961
1225
  }))
962
1226
  ];
963
1227
  var SOURCE_FILE = /\.[cm]?[jt]sx?$/;
1228
+ var QUOTE = "['\"`]";
1229
+ var imports = (body, name) => new RegExp(
1230
+ `(?:from|import|require)\\s*\\(?\\s*${QUOTE}${name.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`)}(?:/[^'"\`]*)?${QUOTE}`
1231
+ ).test(body);
964
1232
  var composedHere = (root, workspaces) => {
965
1233
  const declared = declaredAnywhere(workspaces);
966
1234
  const wanted = COMPOSABLE.filter((one) => declared.has(one.name));
@@ -969,22 +1237,22 @@ var composedHere = (root, workspaces) => {
969
1237
  for (const path of filesUnder(root, (name) => SOURCE_FILE.test(name))) {
970
1238
  let body;
971
1239
  try {
972
- body = readFileSync5(path, "utf8");
1240
+ body = readFileSync7(path, "utf8");
973
1241
  } catch {
974
1242
  continue;
975
1243
  }
976
1244
  for (const one of wanted) {
977
- if (!importing.has(one.name) && body.includes(one.name)) importing.set(one.name, path);
1245
+ if (!importing.has(one.name) && imports(body, one.name)) importing.set(one.name, path);
978
1246
  }
979
1247
  if (importing.size === wanted.length) break;
980
1248
  }
981
1249
  return wanted.map((one) => {
982
1250
  const at = importing.get(one.name);
983
- return at === void 0 ? finding3(
1251
+ return at === void 0 ? finding4(
984
1252
  one.name,
985
1253
  "WARN",
986
1254
  `it is declared here and no source file imports it \u2014 ${one.uncomposed}`
987
- ) : finding3(
1255
+ ) : finding4(
988
1256
  one.name,
989
1257
  "OK",
990
1258
  `composed in ${relativePath(root, at)} \u2014 something here builds what it reports through`
@@ -998,6 +1266,18 @@ var READERS = {
998
1266
  verify: "@geonosis/verify",
999
1267
  walk: "@geonosis/walk"
1000
1268
  };
1269
+ var doorBringing = (root, name, declared) => DOORS.filter((door) => declared.has(door)).find(
1270
+ (door) => Object.hasOwn(dependenciesOf(root, door), name)
1271
+ );
1272
+ var dependenciesOf = (root, name) => {
1273
+ try {
1274
+ return JSON.parse(
1275
+ readFileSync7(join7(packageDirOf(resolveFrom(root, name), name), "package.json"), "utf8")
1276
+ ).dependencies ?? {};
1277
+ } catch {
1278
+ return {};
1279
+ }
1280
+ };
1001
1281
  var resolves = (root, name) => {
1002
1282
  try {
1003
1283
  resolveFrom(root, name);
@@ -1019,7 +1299,7 @@ var declaredAnywhere = (workspaces) => new Set(
1019
1299
  var blocks = (root, config, readers, workspaces) => {
1020
1300
  if (config === void 0) {
1021
1301
  return [
1022
- finding3(
1302
+ finding4(
1023
1303
  GEONOSIS,
1024
1304
  "SKIP",
1025
1305
  "there is no geonosis.json here, so no block says what this repo asks the kit to do"
@@ -1029,11 +1309,21 @@ var blocks = (root, config, readers, workspaces) => {
1029
1309
  const declaredPackages = declaredAnywhere(workspaces);
1030
1310
  return Object.entries(readers).flatMap(([block, name]) => {
1031
1311
  const declared = config[block] !== void 0;
1312
+ const door = doorBringing(root, name, declaredPackages);
1032
1313
  const chosen = declaredPackages.has(name);
1033
1314
  const installed = resolves(root, name);
1315
+ if (declared && !chosen && door !== void 0) {
1316
+ return [
1317
+ finding4(
1318
+ name,
1319
+ "OK",
1320
+ `a "${block}" block, and ${door} brings ${name} to read it \u2014 #177: the root declares the door, not what the door owns`
1321
+ )
1322
+ ];
1323
+ }
1034
1324
  if (declared && !chosen) {
1035
1325
  return [
1036
- finding3(
1326
+ finding4(
1037
1327
  name,
1038
1328
  "WARN",
1039
1329
  `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 +1332,7 @@ var blocks = (root, config, readers, workspaces) => {
1042
1332
  }
1043
1333
  if (declared && !installed) {
1044
1334
  return [
1045
- finding3(
1335
+ finding4(
1046
1336
  name,
1047
1337
  "WARN",
1048
1338
  `geonosis.json has a "${block}" block, a manifest declares ${name}, and it does not resolve here \u2014 run the install`
@@ -1051,7 +1341,7 @@ var blocks = (root, config, readers, workspaces) => {
1051
1341
  }
1052
1342
  if (!declared && chosen && installed) {
1053
1343
  return [
1054
- finding3(
1344
+ finding4(
1055
1345
  name,
1056
1346
  "WARN",
1057
1347
  `${name} is installed and geonosis.json has no "${block}" block \u2014 it runs on its defaults, whatever they are`
@@ -1060,14 +1350,14 @@ var blocks = (root, config, readers, workspaces) => {
1060
1350
  }
1061
1351
  if (!declared && installed) {
1062
1352
  return [
1063
- finding3(
1353
+ finding4(
1064
1354
  name,
1065
1355
  "OK",
1066
1356
  `${name} is here but no manifest declares it \u2014 a transitive dependency this repo did not choose, so it wants no "${block}" block`
1067
1357
  )
1068
1358
  ];
1069
1359
  }
1070
- return declared ? [finding3(name, "OK", `a "${block}" block, and ${name} to read it`)] : [];
1360
+ return declared ? [finding4(name, "OK", `a "${block}" block, and ${name} to read it`)] : [];
1071
1361
  });
1072
1362
  };
1073
1363
  var PLUGIN_DIR_RULE = "biological-architecture/no-unregistered-plugin-dir";
@@ -1083,8 +1373,8 @@ var pluginDirsOf = (level) => {
1083
1373
  return { manifests: options.manifests ?? ["index.ts"], registry, roots: options.roots };
1084
1374
  };
1085
1375
  var pluginDirLayers = (root) => {
1086
- const path = join5(root, ".oxlintrc.json");
1087
- if (!existsSync4(path)) return [];
1376
+ const path = join7(root, ".oxlintrc.json");
1377
+ if (!existsSync6(path)) return [];
1088
1378
  const layers = [];
1089
1379
  for (const layer of layersOf(readConfig(path, root), PLUGIN_DIR_RULE)) {
1090
1380
  const dirs = pluginDirsOf(layer.level);
@@ -1093,7 +1383,7 @@ var pluginDirLayers = (root) => {
1093
1383
  return layers;
1094
1384
  };
1095
1385
  var layerOver = (layers, root, relative) => {
1096
- const at = join5(root, relative);
1386
+ const at = join7(root, relative);
1097
1387
  const inside = readdirSync3(at, { withFileTypes: true }).filter((entry) => entry.isFile()).map((entry) => `${relative}/${entry.name}`);
1098
1388
  const paths = inside.length === 0 ? [relative] : inside;
1099
1389
  const wrapped = layers.map((layer) => ({ files: layer.files, level: layer.dirs }));
@@ -1107,7 +1397,7 @@ var pluginDirs = (root) => {
1107
1397
  const layers = pluginDirLayers(root);
1108
1398
  if (layers.length === 0) {
1109
1399
  return [
1110
- finding3(
1400
+ finding4(
1111
1401
  "plugin directories",
1112
1402
  "SKIP",
1113
1403
  "no-unregistered-plugin-dir is not configured here, so this repo has not said where its integrations live"
@@ -1115,34 +1405,34 @@ var pluginDirs = (root) => {
1115
1405
  ];
1116
1406
  }
1117
1407
  const missing = [...new Set(layers.flatMap((layer) => layer.dirs.registry))].filter(
1118
- (registry) => !existsSync4(join5(root, registry))
1408
+ (registry) => !existsSync6(join7(root, registry))
1119
1409
  );
1120
1410
  if (missing.length > 0) {
1121
1411
  return missing.map(
1122
- (registry) => finding3(registry, "FAIL", "the registry the rule names is not there")
1412
+ (registry) => finding4(registry, "FAIL", "the registry the rule names is not there")
1123
1413
  );
1124
1414
  }
1125
1415
  const source = /* @__PURE__ */ new Map();
1126
1416
  const unreachable = /* @__PURE__ */ new Map();
1127
1417
  for (const rootDir of new Set(layers.flatMap((layer) => layer.dirs.roots))) {
1128
- const at = join5(root, rootDir);
1129
- if (!existsSync4(at)) continue;
1418
+ const at = join7(root, rootDir);
1419
+ if (!existsSync6(at)) continue;
1130
1420
  for (const entry of readdirSync3(at, { withFileTypes: true })) {
1131
1421
  if (!entry.isDirectory()) continue;
1132
1422
  const relative = `${rootDir}/${entry.name}`;
1133
1423
  const governs = layerOver(layers, root, relative);
1134
1424
  if (governs === void 0) continue;
1135
1425
  const key = governs.registry.join(", ");
1136
- const registry = source.get(key) ?? governs.registry.map((half) => readFileSync5(join5(root, half), "utf8")).join("\n");
1426
+ const registry = source.get(key) ?? governs.registry.map((half) => readFileSync7(join7(root, half), "utf8")).join("\n");
1137
1427
  source.set(key, registry);
1138
- const hasManifest = governs.manifests.some((name) => existsSync4(join5(at, entry.name, name)));
1428
+ const hasManifest = governs.manifests.some((name) => existsSync6(join7(at, entry.name, name)));
1139
1429
  if (!hasManifest || registry.includes(entry.name)) continue;
1140
1430
  unreachable.set(key, [...unreachable.get(key) ?? [], relative]);
1141
1431
  }
1142
1432
  }
1143
1433
  return [...source.keys()].toSorted().map((registry) => {
1144
1434
  const named2 = unreachable.get(registry) ?? [];
1145
- return named2.length === 0 ? finding3(registry, "OK", "every directory under the declared roots is named by it") : finding3(
1435
+ return named2.length === 0 ? finding4(registry, "OK", "every directory under the declared roots is named by it") : finding4(
1146
1436
  registry,
1147
1437
  "FAIL",
1148
1438
  `${named2.length} directory(ies) it never names: ${named2.join(", ")}`
@@ -1153,9 +1443,9 @@ var WORKSPACE_YAML = "pnpm-workspace.yaml";
1153
1443
  var HOIST_KEY = "publicHoistPattern";
1154
1444
  var HOIST_REPAIR = "rm -rf node_modules/.modules.yaml node_modules/.pnpm-workspace-state-v1.json && pnpm install";
1155
1445
  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");
1446
+ const path = join7(root, WORKSPACE_YAML);
1447
+ if (!existsSync6(path)) return void 0;
1448
+ const lines = readFileSync7(path, "utf8").split("\n");
1159
1449
  const at = lines.findIndex((line) => new RegExp(`^${HOIST_KEY}\\s*:`).test(line));
1160
1450
  if (at < 0) return void 0;
1161
1451
  const patterns = [];
@@ -1172,7 +1462,7 @@ var publicHoists = (root, workspaces) => {
1172
1462
  const patterns = hoistPatterns(root);
1173
1463
  if (patterns === void 0) {
1174
1464
  return [
1175
- finding3(
1465
+ finding4(
1176
1466
  WORKSPACE_YAML,
1177
1467
  "SKIP",
1178
1468
  `no ${HOIST_KEY} is declared here, so nothing claims a package should be linked at the root`
@@ -1186,21 +1476,21 @@ var publicHoists = (root, workspaces) => {
1186
1476
  const shape = matching(pattern);
1187
1477
  const hoisted = named2.filter((name) => shape.test(name));
1188
1478
  if (hoisted.length === 0) {
1189
- return finding3(
1479
+ return finding4(
1190
1480
  pattern,
1191
1481
  "SKIP",
1192
1482
  `this ${HOIST_KEY} matches no workspace package here, so what it should have linked at the root is a question this cannot answer`
1193
1483
  );
1194
1484
  }
1195
- const pruned = hoisted.filter((name) => !existsSync4(join5(root, "node_modules", name)));
1485
+ const pruned = hoisted.filter((name) => !existsSync6(join7(root, "node_modules", name)));
1196
1486
  if (pruned.length === 0) {
1197
- return finding3(
1487
+ return finding4(
1198
1488
  pattern,
1199
1489
  "OK",
1200
1490
  `every workspace package this ${HOIST_KEY} matches is linked at the root`
1201
1491
  );
1202
1492
  }
1203
- return finding3(
1493
+ return finding4(
1204
1494
  pattern,
1205
1495
  "FAIL",
1206
1496
  `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 +1500,7 @@ var publicHoists = (root, workspaces) => {
1210
1500
  var checkDrift = ({
1211
1501
  readers = READERS,
1212
1502
  root,
1213
- userSettings = join5(homedir(), SETTINGS),
1503
+ userSettings = join7(homedir(), SETTINGS),
1214
1504
  workspaces
1215
1505
  }) => {
1216
1506
  const config = readGeonosis(root);
@@ -1236,21 +1526,21 @@ var checkDrift = ({
1236
1526
  import { spawnSync as spawnSync2 } from "child_process";
1237
1527
  import {
1238
1528
  cpSync,
1239
- existsSync as existsSync5,
1529
+ existsSync as existsSync7,
1240
1530
  mkdirSync,
1241
1531
  mkdtempSync,
1242
- readFileSync as readFileSync6,
1532
+ readFileSync as readFileSync8,
1243
1533
  rmSync,
1244
1534
  writeFileSync
1245
1535
  } from "fs";
1246
1536
  import { tmpdir } from "os";
1247
- import { dirname as dirname2, join as join6, resolve } from "path";
1537
+ import { dirname as dirname2, join as join8, resolve } from "path";
1248
1538
  import { corpusOf, readManifest } from "@geonosis/lint-parity";
1249
1539
  var OFF = /* @__PURE__ */ new Set([0, "0", "allow", "off", false]);
1250
1540
  var severityOf = (level) => Array.isArray(level) ? level[0] : level;
1251
1541
  var enabledRulesOf = (rules, plugin) => Object.keys(rules).filter((id) => id.startsWith(`${plugin}/`) && !OFF.has(severityOf(rules[id]))).toSorted();
1252
1542
  var passes = (findings) => !findings.some((one) => one.verdict === "FAIL" || one.verdict === "UNJUDGED");
1253
- var finding4 = (subject, verdict, message) => ({
1543
+ var finding5 = (subject, verdict, message) => ({
1254
1544
  check: "exercised",
1255
1545
  message,
1256
1546
  subject,
@@ -1262,9 +1552,9 @@ var refusal = (error) => {
1262
1552
  return said2.length > LIMIT ? `${said2.slice(0, LIMIT)}\u2026` : said2;
1263
1553
  };
1264
1554
  var reasonFrom = (config, oxlint) => {
1265
- const dir = mkdtempSync(join6(tmpdir(), "geonosis-doctor-why-"));
1555
+ const dir = mkdtempSync(join8(tmpdir(), "geonosis-doctor-why-"));
1266
1556
  try {
1267
- const probe = join6(dir, "probe.tsx");
1557
+ const probe = join8(dir, "probe.tsx");
1268
1558
  writeFileSync(probe, "export const probe = 1\n");
1269
1559
  const run = spawnSync2(
1270
1560
  oxlint,
@@ -1354,8 +1644,8 @@ var throughProbes = ({
1354
1644
  }) => {
1355
1645
  const refused = /* @__PURE__ */ new Map();
1356
1646
  const placed = /* @__PURE__ */ new Set();
1357
- const dir = mkdtempSync(join6(tmpdir(), "geonosis-doctor-probe-"));
1358
- const here = join6(dir, "corpus");
1647
+ const dir = mkdtempSync(join8(tmpdir(), "geonosis-doctor-probe-"));
1648
+ const here = join8(dir, "corpus");
1359
1649
  try {
1360
1650
  cpSync(corpus, here, { recursive: true });
1361
1651
  for (const rule of silent) {
@@ -1392,7 +1682,7 @@ var throughProbes = ({
1392
1682
  mounted = rehomed;
1393
1683
  }
1394
1684
  for (const file of mounted) {
1395
- const at2 = join6(here, file.path);
1685
+ const at2 = join8(here, file.path);
1396
1686
  if (placed.has(at2)) continue;
1397
1687
  placed.add(at2);
1398
1688
  mkdirSync(dirname2(at2), { recursive: true });
@@ -1405,12 +1695,12 @@ var throughProbes = ({
1405
1695
  refused.set(rule, String(error.message));
1406
1696
  }
1407
1697
  }
1408
- const probeConfig = JSON.parse(readFileSync6(config.path, "utf8"));
1698
+ const probeConfig = JSON.parse(readFileSync8(config.path, "utf8"));
1409
1699
  const configDir = dirname2(config.path);
1410
1700
  probeConfig.jsPlugins = (probeConfig.jsPlugins ?? []).map(
1411
1701
  (spec) => spec.startsWith(".") || spec.startsWith("/") ? resolve(configDir, spec) : packageDirOf(resolveFrom(configDir, spec), spec)
1412
1702
  );
1413
- const at = join6(here, ".oxlintrc-geonosis-probe.json");
1703
+ const at = join8(here, ".oxlintrc-geonosis-probe.json");
1414
1704
  writeFileSync(at, JSON.stringify(probeConfig, null, 2));
1415
1705
  const run = spawnSync2(
1416
1706
  oxlint,
@@ -1440,14 +1730,14 @@ var checkExercised = async ({
1440
1730
  repoCorpus,
1441
1731
  root
1442
1732
  }) => {
1443
- const said2 = (verdict, message) => finding4(config.relative, verdict, message);
1444
- if (repoCorpus !== void 0 && !existsSync5(repoCorpus)) {
1733
+ const said2 = (verdict, message) => finding5(config.relative, verdict, message);
1734
+ if (repoCorpus !== void 0 && !existsSync7(repoCorpus)) {
1445
1735
  return said2(
1446
1736
  "FAIL",
1447
1737
  `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
1738
  );
1449
1739
  }
1450
- if (!existsSync5(corpus)) {
1740
+ if (!existsSync7(corpus)) {
1451
1741
  return said2(
1452
1742
  "SKIP",
1453
1743
  `the plugin loaded from here ships no corpus at ${relativeToRoot(root, corpus)} \u2014 nothing declares which rules it can be evidence about`
@@ -1520,341 +1810,154 @@ var checkExercised = async ({
1520
1810
  );
1521
1811
  }
1522
1812
  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(", ")}`;
1524
- const unknowable = loadedProbes === null ? unprobed.map(
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`
1526
- ) : [];
1527
- if (unplaceable.length > 0 || unknowable.length > 0) {
1528
- return said2(
1529
- "UNJUDGED",
1530
- named([...unknowable, ...unplaceable]) + (kitNote === "" ? "" : ` \u2014 and ${kitNote}`)
1531
- );
1532
- }
1533
- if (kitNote !== "") return said2("WARN", kitNote);
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`;
1535
- return said2(
1536
- "OK",
1537
- `${enabled.length} enabled, ${enabled.length} exercised${where} \u2014 ${through}. ${BASE}`
1538
- );
1539
- };
1540
-
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
- );
1564
- }
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
- );
1571
- }
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")
1813
+ 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(", ")}`;
1814
+ const unknowable = loadedProbes === null ? unprobed.map(
1815
+ (rule) => `${rule} declares no probe, so ${optionSetsOf(config, rule).every((options) => options.length === 0) ? "its scope" : under(config, rule)} does not reach the corpus`
1816
+ ) : [];
1817
+ if (unplaceable.length > 0 || unknowable.length > 0) {
1818
+ return said2(
1819
+ "UNJUDGED",
1820
+ named([...unknowable, ...unplaceable]) + (kitNote === "" ? "" : ` \u2014 and ${kitNote}`)
1699
1821
  );
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
1822
  }
1823
+ if (kitNote !== "") return said2("SKIP", kitNote);
1824
+ 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`;
1825
+ return said2(
1826
+ "OK",
1827
+ `${enabled.length} enabled, ${enabled.length} exercised${where} \u2014 ${through}. ${BASE}`
1828
+ );
1704
1829
  };
1705
- var finding6 = (subject, verdict, message) => ({
1706
- check: "group",
1830
+
1831
+ // src/envelope.ts
1832
+ import { existsSync as existsSync8, readdirSync as readdirSync4, readFileSync as readFileSync9 } from "fs";
1833
+ import { join as join9 } from "path";
1834
+ var ENVELOPES_DIR = ".geonosis/envelopes";
1835
+ var THIS_TOOL = "doctor";
1836
+ var OWN = `${THIS_TOOL}.json`;
1837
+ 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)";
1838
+ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1839
+ var finding6 = (verdict, subject, message) => ({
1840
+ check: "envelope",
1707
1841
  message,
1708
1842
  subject,
1709
1843
  verdict
1710
1844
  });
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,
1845
+ var lengthOf = (value) => Array.isArray(value) ? value.length : void 0;
1846
+ 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";
1847
+ var judge = (subject, name, parsed) => {
1848
+ if (!isRecord2(parsed)) return finding6("FAIL", subject, `is not an object. ${NEXT}`);
1849
+ const tool = parsed["tool"];
1850
+ if (typeof tool !== "string" || tool.trim() === "") {
1851
+ return finding6(
1852
+ "FAIL",
1853
+ subject,
1854
+ `names no tool, so nothing here says which run it is about. ${NEXT}`
1855
+ );
1856
+ }
1857
+ if (tool !== name) {
1858
+ return finding6(
1859
+ "FAIL",
1860
+ subject,
1861
+ `is written by "${tool}", not "${name}" \u2014 a tool writing another tool's envelope leaves both of their numbers unattributable. ${NEXT}`
1862
+ );
1863
+ }
1864
+ const version = parsed["version"];
1865
+ if (typeof version !== "string" || version.trim() === "") {
1866
+ return finding6(
1867
+ "FAIL",
1868
+ subject,
1869
+ `${tool}: names no version, so nothing dates this run and a stale envelope reads exactly like a fresh one. ${NEXT}`
1870
+ );
1871
+ }
1872
+ const considered = parsed["considered"];
1873
+ const read = parsed["read"];
1874
+ const refused = lengthOf(parsed["refused"]);
1875
+ const excused = lengthOf(parsed["excused"]);
1876
+ if (typeof considered !== "number" || typeof read !== "number" || refused === void 0 || excused === void 0) {
1877
+ return finding6(
1878
+ "FAIL",
1879
+ subject,
1880
+ `${tool}: has no considered/read/refused/excused to check \u2014 the four numbers ARE the envelope, and a file without them measures nothing. ${NEXT}`
1881
+ );
1882
+ }
1883
+ const accounted = read + refused + excused;
1884
+ return considered === accounted ? finding6(
1885
+ "OK",
1886
+ subject,
1887
+ `${tool} considered ${considered} and accounts for all of them \u2014 ${read} read + ${refused} refused + ${excused} excused`
1888
+ ) : finding6(
1718
1889
  "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`
1890
+ subject,
1891
+ `${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}`
1720
1892
  );
1721
1893
  };
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
- );
1894
+ var checkEnvelopes = ({ root }) => {
1895
+ const dir = join9(root, ENVELOPES_DIR);
1896
+ const files = existsSync8(dir) ? readdirSync4(dir).filter((name) => name.endsWith(".json") && name !== OWN).toSorted() : [];
1897
+ if (files.length === 0) return [finding6("SKIP", ENVELOPES_DIR, NO_ENVELOPES)];
1898
+ return files.map((name) => {
1899
+ const subject = `${ENVELOPES_DIR}/${name}`;
1900
+ let parsed;
1901
+ try {
1902
+ parsed = JSON.parse(readFileSync9(join9(dir, name), "utf8"));
1903
+ } catch (error) {
1904
+ return finding6("FAIL", subject, `is not readable JSON: ${error.message}. ${NEXT}`);
1905
+ }
1906
+ return judge(subject, name.replace(/\.json$/, ""), parsed);
1907
+ });
1730
1908
  };
1731
- var DOORS = ["@geonosis/cli", "geonosis"];
1732
- var manifestOf = (dir, name) => {
1909
+
1910
+ // src/claude-plugin.ts
1911
+ import { existsSync as existsSync9, readFileSync as readFileSync10 } from "fs";
1912
+ import { homedir as homedir2 } from "os";
1913
+ import { join as join10 } from "path";
1914
+ var INSTALLED_PLUGINS = ".claude/plugins/installed_plugins.json";
1915
+ var PLUGIN_NAME = "geonosis";
1916
+ var installedPluginVersions = (home, name) => {
1917
+ const at = join10(home, INSTALLED_PLUGINS);
1918
+ if (!existsSync9(at)) return [];
1919
+ let record;
1733
1920
  try {
1734
- return JSON.parse(
1735
- readFileSync9(join9(packageDirOf(resolveFrom(dir, name), name), "package.json"), "utf8")
1736
- );
1921
+ record = JSON.parse(readFileSync10(at, "utf8"));
1737
1922
  } catch {
1738
- return void 0;
1923
+ return [];
1739
1924
  }
1925
+ return Object.entries(record.plugins ?? {}).filter(([key]) => (key.split("@")[0] ?? key) === name).flatMap(([, value]) => Array.isArray(value) ? value : []).flatMap((one) => typeof one.version === "string" ? [one.version] : []);
1740
1926
  };
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) {
1927
+ var checkClaudePlugin = ({
1928
+ home = homedir2(),
1929
+ name = PLUGIN_NAME,
1930
+ train
1931
+ }) => {
1932
+ if (train === void 0) return [];
1933
+ const installed = installedPluginVersions(home, name);
1934
+ if (installed.length === 0) {
1777
1935
  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
- )
1936
+ {
1937
+ check: "loaded",
1938
+ message: `this machine has no "${name}" plugin installed, so none of this release's hooks, agents or skills is loaded in any session here \u2014 a bump moved the packages and nothing moved them. Install it, or say out loud that this repo runs without the plugin`,
1939
+ subject: `${name} (Claude plugin)`,
1940
+ verdict: "WARN"
1941
+ }
1783
1942
  ];
1784
1943
  }
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
- );
1944
+ const drifted = installed.filter((one) => one !== train);
1945
+ if (drifted.length === 0) return [];
1946
+ return [
1947
+ {
1948
+ check: "loaded",
1949
+ message: `the installed plugin is ${drifted.join(", ")} and the packages here are ${train} \u2014 the plugin carries the hooks, the agents and the skills, and it moves on \`claude plugin update\`, never on an install. Every session in this repo is running the older ones`,
1950
+ subject: `${name} (Claude plugin)`,
1951
+ verdict: "WARN"
1849
1952
  }
1850
- }
1851
- return findings;
1953
+ ];
1852
1954
  };
1853
1955
 
1854
1956
  // src/loaded.ts
1855
- import { readFileSync as readFileSync10 } from "fs";
1856
- import { join as join10, sep as sep3 } from "path";
1957
+ import { readFileSync as readFileSync11 } from "fs";
1958
+ import { join as join11, sep as sep3 } from "path";
1857
1959
  var SCOPE = "@geonosis/";
1960
+ 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
1961
  var BLOCKS = [
1859
1962
  "dependencies",
1860
1963
  "devDependencies",
@@ -1966,7 +2069,7 @@ var oneConfig = async ({
1966
2069
  }
1967
2070
  return held ? said2("OK", `loaded ${loaded.version} = declared ${declared.spec} (${declared.at})`) : said2(
1968
2071
  "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`
2072
+ `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
2073
  );
1971
2074
  };
1972
2075
  var labelOf2 = (workspace) => workspace.relative === "" ? "root" : workspace.relative;
@@ -2005,7 +2108,7 @@ var shipperOf = (root, bin) => {
2005
2108
  const name = bin === "geonosis" ? "@geonosis/cli" : `@geonosis/${bin.replace(/^geonosis-/, "")}`;
2006
2109
  try {
2007
2110
  const manifest = JSON.parse(
2008
- readFileSync10(join10(packageDirOf(resolveFrom(root, name), name), "package.json"), "utf8")
2111
+ readFileSync11(join11(packageDirOf(resolveFrom(root, name), name), "package.json"), "utf8")
2009
2112
  );
2010
2113
  const declared = manifest.bin;
2011
2114
  return typeof declared === "object" && !Object.hasOwn(declared, bin) ? "" : name;
@@ -2053,6 +2156,7 @@ var gitHooks = (root) => {
2053
2156
  };
2054
2157
  var checkLoaded = async ({
2055
2158
  configs,
2159
+ home,
2056
2160
  root,
2057
2161
  workspaces
2058
2162
  }) => {
@@ -2071,12 +2175,23 @@ var checkLoaded = async ({
2071
2175
  for (const specifier of [...specifiers].toSorted()) {
2072
2176
  findings.push(await copiesOf({ root, specifier, workspaces }));
2073
2177
  }
2178
+ findings.push(...checkClaudePlugin({ home, train: await trainVersion(root, [...specifiers]) }));
2074
2179
  return findings;
2075
2180
  };
2181
+ var trainVersion = async (root, specifiers) => {
2182
+ for (const specifier of specifiers.toSorted()) {
2183
+ try {
2184
+ return await pluginVersionOf(resolveFrom(root, specifier));
2185
+ } catch {
2186
+ continue;
2187
+ }
2188
+ }
2189
+ return void 0;
2190
+ };
2076
2191
 
2077
2192
  // src/observability.ts
2078
- import { readFileSync as readFileSync11 } from "fs";
2079
- import { join as join11 } from "path";
2193
+ import { readFileSync as readFileSync12 } from "fs";
2194
+ import { join as join12 } from "path";
2080
2195
  var GEONOSIS_FILE2 = "geonosis.json";
2081
2196
  var REACHES_NOTHING = /* @__PURE__ */ new Set(["console", "memory", "noop", "none", "null", "swallowing"]);
2082
2197
  var DEFAULT_MAX_AGE_SECONDS = 3600;
@@ -2090,7 +2205,7 @@ var finding8 = (verdict, subject, message) => ({
2090
2205
  var readGeonosis2 = (root) => {
2091
2206
  let text;
2092
2207
  try {
2093
- text = readFileSync11(join11(root, GEONOSIS_FILE2), "utf8");
2208
+ text = readFileSync12(join12(root, GEONOSIS_FILE2), "utf8");
2094
2209
  } catch {
2095
2210
  return { present: false };
2096
2211
  }
@@ -2163,7 +2278,7 @@ var ageFinding = (config, root, now) => {
2163
2278
  const maxAgeSeconds = typeof config.maxAgeSeconds === "number" && config.maxAgeSeconds > 0 ? config.maxAgeSeconds : DEFAULT_MAX_AGE_SECONDS;
2164
2279
  let record;
2165
2280
  try {
2166
- record = JSON.parse(readFileSync11(join11(root, file), "utf8"));
2281
+ record = JSON.parse(readFileSync12(join12(root, file), "utf8"));
2167
2282
  } catch (error) {
2168
2283
  return finding8(
2169
2284
  "FAIL",
@@ -2240,8 +2355,8 @@ var checkObservability = async ({
2240
2355
  };
2241
2356
 
2242
2357
  // src/runner.ts
2243
- import { existsSync as existsSync9, readdirSync as readdirSync5, readFileSync as readFileSync12 } from "fs";
2244
- import { join as join12 } from "path";
2358
+ import { existsSync as existsSync10, readdirSync as readdirSync5, readFileSync as readFileSync13 } from "fs";
2359
+ import { join as join13 } from "path";
2245
2360
  var TEST_FAILURES = "testFailures";
2246
2361
  var RUNS_A_RUNNER = /(?:^|[\s;&|(])(?:npx\s+|bunx\s+|pnpm\s+(?:exec\s+)?)?(?:vitest|bun\s+test)(?:\s|$)/;
2247
2362
  var RUNS_BUN_TEST = /(?:^|[\s;&|(])(?:bunx\s+)?bun\s+test(?:\s|$)/;
@@ -2267,17 +2382,17 @@ var keyOf = (entry) => stringOf(entry.key) === "" ? TEST_FAILURES : stringOf(ent
2267
2382
  var PROVES = /--prove\b/;
2268
2383
  var everythingRun2 = (root, workspaces) => {
2269
2384
  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) => {
2385
+ const dir = join13(root, ".github/workflows");
2386
+ const workflows = (existsSync10(dir) ? readdirSync5(dir) : []).filter((name) => name.endsWith(".yml") || name.endsWith(".yaml")).map((name) => {
2272
2387
  try {
2273
- return readFileSync12(join12(dir, name), "utf8");
2388
+ return readFileSync13(join13(dir, name), "utf8");
2274
2389
  } catch {
2275
2390
  return "";
2276
2391
  }
2277
2392
  });
2278
2393
  let tiers = {};
2279
2394
  try {
2280
- const config = JSON.parse(readFileSync12(join12(root, "geonosis.json"), "utf8"));
2395
+ const config = JSON.parse(readFileSync13(join13(root, "geonosis.json"), "utf8"));
2281
2396
  tiers = config.verify ?? {};
2282
2397
  } catch {
2283
2398
  }
@@ -2358,8 +2473,8 @@ var checkRunner = ({
2358
2473
  };
2359
2474
 
2360
2475
  // src/doctor.ts
2361
- import { homedir as homedir2 } from "os";
2362
- import { join as join14 } from "path";
2476
+ import { homedir as homedir3 } from "os";
2477
+ import { join as join15 } from "path";
2363
2478
  import { resolveOxlint } from "@geonosis/lint-parity";
2364
2479
 
2365
2480
  // src/engine.ts
@@ -2373,9 +2488,8 @@ var declaredIn3 = (manifest) => new Set(
2373
2488
  ].flatMap((block) => Object.keys(block ?? {}))
2374
2489
  );
2375
2490
  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
- );
2491
+ var labelOf3 = (workspace) => workspace.relative === "" ? "root" : workspace.relative;
2492
+ var governedBy = (dir, workspaces) => workspaces.filter((one) => holds3(one.dir, dir) || holds3(dir, one.dir));
2379
2493
  var enabledHere2 = (config, plugin) => [
2380
2494
  .../* @__PURE__ */ new Set([
2381
2495
  ...enabledRulesOf(config.rules, plugin),
@@ -2387,18 +2501,36 @@ var checkEngines = async ({
2387
2501
  entry,
2388
2502
  workspaces
2389
2503
  }) => {
2390
- const read = await presumptionsOf(entry).catch(() => void 0);
2391
- if (read === void 0 || read.namespace === "" || Object.keys(read.presumed).length === 0) {
2504
+ const plugin = await presumptionsOf(entry).catch(() => void 0);
2505
+ if (plugin === void 0 || plugin.namespace === "" || Object.keys(plugin.presumed).length === 0 && Object.keys(plugin.floors).length === 0) {
2392
2506
  return [];
2393
2507
  }
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 [];
2508
+ const pending = (rule) => {
2509
+ const floor = plugin.floors[rule.slice(plugin.namespace.length + 1)];
2510
+ if (floor === void 0) return [];
2511
+ return [
2512
+ {
2513
+ check: "exercised",
2514
+ message: `it names a pattern the kit does not ship yet (${floor.name}) \u2014 the rule is the requirement and there is nothing to install behind it, so a repo enabling it hand-writes the floor to satisfy the lint. Wait for the floor, or keep the pattern of your own and know that is what it is`,
2515
+ subject: `${config.relative} \u2192 ${rule}`,
2516
+ verdict: "WARN"
2517
+ }
2518
+ ];
2519
+ };
2520
+ const governed = governedBy(config.dir, workspaces);
2521
+ const declared = new Set(governed.flatMap((one) => [...declaredIn3(one.manifest)]));
2522
+ const asked = governed.map(labelOf3).join(", ");
2523
+ return enabledHere2(config, plugin.namespace).flatMap((rule) => {
2524
+ const floorless = pending(rule);
2525
+ const presumes = plugin.presumed[rule.slice(plugin.namespace.length + 1)];
2526
+ if (presumes === void 0 || presumes.packages.some((name) => declared.has(name))) {
2527
+ return floorless;
2528
+ }
2398
2529
  return [
2530
+ ...floorless,
2399
2531
  {
2400
2532
  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`,
2533
+ 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
2534
  subject: `${config.relative} \u2192 ${rule}`,
2403
2535
  verdict: "WARN"
2404
2536
  }
@@ -2407,8 +2539,8 @@ var checkEngines = async ({
2407
2539
  };
2408
2540
 
2409
2541
  // src/rails.ts
2410
- import { existsSync as existsSync10, readFileSync as readFileSync13 } from "fs";
2411
- import { join as join13 } from "path";
2542
+ import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
2543
+ import { join as join14 } from "path";
2412
2544
  var GEONOSIS2 = "geonosis.json";
2413
2545
  var PROJECT = ".claude/settings.json";
2414
2546
  var LOCAL = ".claude/settings.local.json";
@@ -2434,10 +2566,10 @@ var networkIn = (parsed) => {
2434
2566
  };
2435
2567
  };
2436
2568
  var sourceAt = (path, label) => {
2437
- if (!existsSync10(path)) return { network: void 0, path: label, unreadable: false };
2569
+ if (!existsSync11(path)) return { network: void 0, path: label, unreadable: false };
2438
2570
  try {
2439
2571
  return {
2440
- network: networkIn(JSON.parse(readFileSync13(path, "utf8"))),
2572
+ network: networkIn(JSON.parse(readFileSync14(path, "utf8"))),
2441
2573
  path: label,
2442
2574
  unreadable: false
2443
2575
  };
@@ -2446,11 +2578,11 @@ var sourceAt = (path, label) => {
2446
2578
  }
2447
2579
  };
2448
2580
  var declaredEgress = (root) => {
2449
- const at = join13(root, GEONOSIS2);
2450
- if (!existsSync10(at)) return void 0;
2581
+ const at = join14(root, GEONOSIS2);
2582
+ if (!existsSync11(at)) return void 0;
2451
2583
  let parsed;
2452
2584
  try {
2453
- parsed = JSON.parse(readFileSync13(at, "utf8"));
2585
+ parsed = JSON.parse(readFileSync14(at, "utf8"));
2454
2586
  } catch {
2455
2587
  return void 0;
2456
2588
  }
@@ -2462,8 +2594,8 @@ var declaredEgress = (root) => {
2462
2594
  };
2463
2595
  };
2464
2596
  var deniedEgress = (root) => {
2465
- const at = join13(root, RUN_RECORD);
2466
- if (!existsSync10(at)) {
2597
+ const at = join14(root, RUN_RECORD);
2598
+ if (!existsSync11(at)) {
2467
2599
  return [
2468
2600
  finding10(
2469
2601
  "deniedEgress",
@@ -2474,7 +2606,7 @@ var deniedEgress = (root) => {
2474
2606
  }
2475
2607
  let parsed;
2476
2608
  try {
2477
- parsed = JSON.parse(readFileSync13(at, "utf8"));
2609
+ parsed = JSON.parse(readFileSync14(at, "utf8"));
2478
2610
  } catch (error) {
2479
2611
  return [
2480
2612
  finding10(
@@ -2515,8 +2647,8 @@ var checkRails = ({
2515
2647
  const sources = [
2516
2648
  sourceAt(managedSettings, managedSettings),
2517
2649
  sourceAt(userSettings, userSettings),
2518
- sourceAt(join13(root, PROJECT), PROJECT),
2519
- sourceAt(join13(root, LOCAL), LOCAL)
2650
+ sourceAt(join14(root, PROJECT), PROJECT),
2651
+ sourceAt(join14(root, LOCAL), LOCAL)
2520
2652
  ];
2521
2653
  const unreadable = sources.filter((one) => one.unreadable).map(
2522
2654
  (one) => finding10(
@@ -2552,6 +2684,15 @@ var missingOption = (error) => {
2552
2684
  const named2 = error.option;
2553
2685
  return typeof named2 === "string" ? named2 : HEADER.exec(String(error.message))?.[1];
2554
2686
  };
2687
+ var refusesBare = (rule) => {
2688
+ try {
2689
+ rule.create({ options: [], report: () => {
2690
+ } });
2691
+ return false;
2692
+ } catch (error) {
2693
+ return missingOption(error) !== void 0;
2694
+ }
2695
+ };
2555
2696
  var configured = (config, namespace) => [config.rules, ...config.overrides.map((one) => one.rules)].flatMap(
2556
2697
  (rules) => enabledRulesOf(rules, namespace).map((id) => [id, optionsOf2(rules[id])])
2557
2698
  );
@@ -2563,26 +2704,37 @@ var checkRequiredOptions = async ({
2563
2704
  const namespace = loaded?.default?.meta?.name;
2564
2705
  const rules = loaded?.default?.rules;
2565
2706
  if (typeof namespace !== "string" || namespace === "" || rules === void 0) return [];
2566
- return configured(config, namespace).flatMap(([id, options]) => {
2707
+ const enabled = configured(config, namespace);
2708
+ if (enabled.length === 0) return [];
2709
+ const missing = [];
2710
+ let gated = 0;
2711
+ for (const [id, options] of enabled) {
2567
2712
  const rule = rules[id.slice(namespace.length + 1)];
2568
- if (rule === void 0) return [];
2713
+ if (rule === void 0) continue;
2714
+ if (refusesBare(rule)) gated += 1;
2569
2715
  try {
2570
2716
  rule.create({ options, report: () => {
2571
2717
  } });
2572
- return [];
2573
2718
  } catch (error) {
2574
2719
  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
- ];
2720
+ if (option === void 0) continue;
2721
+ missing.push({
2722
+ check: "loaded",
2723
+ 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`,
2724
+ subject: `${config.relative} \u2192 ${id}`,
2725
+ verdict: "FAIL"
2726
+ });
2584
2727
  }
2585
- });
2728
+ }
2729
+ if (missing.length > 0) return missing;
2730
+ return [
2731
+ {
2732
+ check: "loaded",
2733
+ message: `${gated} rules require options, ${gated} carry them`,
2734
+ subject: config.relative,
2735
+ verdict: "OK"
2736
+ }
2737
+ ];
2586
2738
  };
2587
2739
 
2588
2740
  // src/doctor.ts
@@ -2611,7 +2763,7 @@ var exercisedOf = ({
2611
2763
  ];
2612
2764
  }
2613
2765
  const own = repoCorpus !== void 0 && config.relative === CONFIG_FILE ? repoCorpus : void 0;
2614
- const [reach, engines, options] = await Promise.all([
2766
+ const [reach, engines] = await Promise.all([
2615
2767
  checkExercised({
2616
2768
  config,
2617
2769
  corpus,
@@ -2622,15 +2774,28 @@ var exercisedOf = ({
2622
2774
  }),
2623
2775
  // #159: the rules whose engine this tree has not got. Beside the reach line rather than
2624
2776
  // 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 })
2777
+ checkEngines({ config, entry, workspaces })
2629
2778
  ]);
2630
- return [reach, ...engines, ...options];
2779
+ return [reach, ...engines];
2631
2780
  })
2632
2781
  )
2633
2782
  );
2783
+ var requiredOptionsOf = async (configs) => {
2784
+ const found = await Promise.all(
2785
+ configs.flatMap(
2786
+ (config) => config.jsPlugins.filter((name) => name.startsWith(SCOPE)).map(async (specifier) => {
2787
+ let entry;
2788
+ try {
2789
+ entry = resolveFrom(config.dir, specifier);
2790
+ } catch {
2791
+ return [];
2792
+ }
2793
+ return checkRequiredOptions({ config, entry });
2794
+ })
2795
+ )
2796
+ );
2797
+ return found.flat();
2798
+ };
2634
2799
  var skip = (message) => [
2635
2800
  { check: "baseline", message, subject: RATCHET_FILE, verdict: "SKIP" }
2636
2801
  ];
@@ -2654,6 +2819,7 @@ var countsOf = (findings) => findings.reduce((counts, one) => ({ ...counts, [one
2654
2819
  });
2655
2820
  var runDoctor = async ({
2656
2821
  baseline,
2822
+ home = homedir3(),
2657
2823
  only,
2658
2824
  oxlint,
2659
2825
  root,
@@ -2668,7 +2834,13 @@ var runDoctor = async ({
2668
2834
  }
2669
2835
  const asked = (check) => only === void 0 || only.includes(check);
2670
2836
  const runs = [
2671
- ["loaded", () => checkLoaded({ configs, root, workspaces })],
2837
+ [
2838
+ "loaded",
2839
+ async () => [
2840
+ ...await checkLoaded({ configs, home, root, workspaces }),
2841
+ ...await requiredOptionsOf(configs)
2842
+ ]
2843
+ ],
2672
2844
  [
2673
2845
  "group",
2674
2846
  () => {
@@ -2699,16 +2871,22 @@ var runDoctor = async ({
2699
2871
  ["observability", () => checkObservability({ now: Date.now(), root })],
2700
2872
  ["drift", () => checkDrift({ root, workspaces })],
2701
2873
  ["deployed", () => checkDeployed({ root })],
2702
- ["rails", () => checkRails({ root, userSettings: join14(homedir2(), USER_SETTINGS) })]
2874
+ ["rails", () => checkRails({ root, userSettings: join15(home, USER_SETTINGS) })]
2703
2875
  ];
2704
2876
  const collected = [];
2705
- for (const [check, run] of runs) if (asked(check)) collected.push(...await run());
2877
+ const ran = [];
2878
+ for (const [check, run] of runs) {
2879
+ if (!asked(check)) continue;
2880
+ ran.push(check);
2881
+ collected.push(...await run());
2882
+ }
2706
2883
  const found = ordered(collected);
2707
2884
  const findings = strict ? found.map((one) => one.verdict === "WARN" ? { ...one, verdict: "FAIL" } : one) : found;
2708
2885
  return {
2709
2886
  counts: countsOf(findings),
2710
2887
  findings,
2711
2888
  ok: passes(findings),
2889
+ ran,
2712
2890
  root
2713
2891
  };
2714
2892
  };
@@ -2765,6 +2943,8 @@ export {
2765
2943
  DEPLOYED_FILE,
2766
2944
  NOT_WRITTEN,
2767
2945
  checkDeployed,
2946
+ GEONOSIS_FILE,
2947
+ repoCorpusOf,
2768
2948
  CHECKS,
2769
2949
  DoctorError,
2770
2950
  resolveFrom,
@@ -2772,6 +2952,10 @@ export {
2772
2952
  pluginVersionOf,
2773
2953
  corpusOfPlugin,
2774
2954
  relativeToRoot,
2955
+ FIXED_GROUP,
2956
+ KIT_GROUP,
2957
+ declaredGroupsOf,
2958
+ checkGroup,
2775
2959
  COMPOSITION_ROOT,
2776
2960
  FLOOR_PACKAGES,
2777
2961
  READERS,
@@ -2781,12 +2965,10 @@ export {
2781
2965
  ENVELOPES_DIR,
2782
2966
  NO_ENVELOPES,
2783
2967
  checkEnvelopes,
2784
- GEONOSIS_FILE,
2785
- repoCorpusOf,
2786
- FIXED_GROUP,
2787
- KIT_GROUP,
2788
- declaredGroupsOf,
2789
- checkGroup,
2968
+ INSTALLED_PLUGINS,
2969
+ PLUGIN_NAME,
2970
+ installedPluginVersions,
2971
+ checkClaudePlugin,
2790
2972
  SCOPE,
2791
2973
  satisfies,
2792
2974
  declaredFor,