@geonosis/doctor 1.1.0 → 1.3.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.
@@ -23,19 +23,28 @@ var walk = (dir, onFile) => {
23
23
  }
24
24
  };
25
25
  var parse = (path) => JSON.parse(readFileSync(path, "utf8"));
26
+ var stringsOf = (value) => Array.isArray(value) ? value.filter((one) => typeof one === "string") : [];
27
+ var rulesOf = (value) => typeof value === "object" && value !== null ? value : {};
28
+ var overridesOf = (value) => Array.isArray(value) ? value.filter((one) => typeof one === "object" && one !== null).map((one) => ({ files: stringsOf(one.files), rules: rulesOf(one.rules) })) : [];
26
29
  var readConfig = (path, root) => {
27
30
  const dir = join(path, "..");
28
31
  const relative = relativePath(root, path);
29
32
  try {
30
33
  const config = parse(path);
31
- const jsPlugins = Array.isArray(config.jsPlugins) ? config.jsPlugins.filter((one) => typeof one === "string") : [];
32
- const rules = typeof config.rules === "object" && config.rules !== null ? config.rules : {};
33
- return { dir, jsPlugins, path, relative, rules };
34
+ return {
35
+ dir,
36
+ jsPlugins: stringsOf(config.jsPlugins),
37
+ overrides: overridesOf(config.overrides),
38
+ path,
39
+ relative,
40
+ rules: rulesOf(config.rules)
41
+ };
34
42
  } catch (error) {
35
43
  return {
36
44
  dir,
37
45
  error: `could not read it: ${error.message}`,
38
46
  jsPlugins: [],
47
+ overrides: [],
39
48
  path,
40
49
  relative,
41
50
  rules: {}
@@ -63,6 +72,14 @@ var discoverWorkspaces = (root) => {
63
72
  });
64
73
  return found.toSorted((a, b) => a.relative.localeCompare(b.relative));
65
74
  };
75
+ var TEST_FILE = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
76
+ var testFilesUnder = (dir) => {
77
+ const found = [];
78
+ walk(dir, (path, name) => {
79
+ if (TEST_FILE.test(name)) found.push(path);
80
+ });
81
+ return found;
82
+ };
66
83
  var readRatchet = (root) => {
67
84
  const path = join(root, RATCHET_FILE);
68
85
  try {
@@ -324,7 +341,28 @@ import { existsSync as existsSync2, readFileSync as readFileSync3, realpathSync
324
341
  import { createRequire } from "module";
325
342
  import { dirname, join as join3 } from "path";
326
343
  import { pathToFileURL } from "url";
327
- var resolveFrom = (dir, specifier) => createRequire(join3(dir, "noop.js")).resolve(specifier);
344
+ var packageNameOf = (specifier) => {
345
+ const parts2 = specifier.split("/");
346
+ return specifier.startsWith("@") ? parts2.slice(0, 2).join("/") : parts2[0] ?? specifier;
347
+ };
348
+ var resolveFrom = (dir, specifier) => {
349
+ if (specifier.startsWith(".") || specifier.startsWith("/")) {
350
+ return createRequire(join3(dir, "noop.js")).resolve(specifier);
351
+ }
352
+ const name = packageNameOf(specifier);
353
+ let at = dir;
354
+ for (; ; ) {
355
+ if (existsSync2(join3(at, "node_modules", name, "package.json"))) {
356
+ return createRequire(join3(at, "noop.js")).resolve(specifier);
357
+ }
358
+ const parent = dirname(at);
359
+ if (parent === at) break;
360
+ at = parent;
361
+ }
362
+ throw new DoctorError(
363
+ `${specifier} does not resolve from ${dir} \u2014 no node_modules on the way up carries ${name}`
364
+ );
365
+ };
328
366
  var packageDirOf = (entry, name) => {
329
367
  let dir = dirname(entry);
330
368
  for (; ; ) {
@@ -353,13 +391,17 @@ var pluginVersionOf = async (entry) => {
353
391
  }
354
392
  return version;
355
393
  };
394
+ var filesOf = (answer) => Array.isArray(answer.files) ? answer.files : [answer];
356
395
  var probesOf = async (entry, plugin) => {
357
396
  const loaded = await import(pathToFileURL(entry).href);
358
397
  return Object.fromEntries(
359
- Object.entries(loaded.default?.rules ?? {}).filter(([, rule]) => typeof rule?.probe === "function").map(([name, rule]) => [
360
- `${plugin}/${name}`,
361
- rule.probe
362
- ])
398
+ Object.entries(loaded.default?.rules ?? {}).filter(([, rule]) => typeof rule?.probe === "function").map(([name, rule]) => {
399
+ const declared = rule.probe;
400
+ return [
401
+ `${plugin}/${name}`,
402
+ (options) => filesOf(declared(options))
403
+ ];
404
+ })
363
405
  );
364
406
  };
365
407
  var corpusOfPlugin = (from, specifier) => join3(packageDirOf(resolveFrom(from, specifier), specifier), "corpus");
@@ -373,15 +415,80 @@ var real = (path) => {
373
415
  var relativeToRoot = (root, path) => relativePath(real(root), real(path));
374
416
 
375
417
  // src/drift.ts
376
- import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
418
+ import { closeSync, existsSync as existsSync3, openSync, readdirSync as readdirSync2, readFileSync as readFileSync4, readSync } from "fs";
419
+ import { homedir } from "os";
377
420
  import { join as join4, sep as sep2 } from "path";
421
+
422
+ // src/overrides.ts
423
+ var SPECIAL = /* @__PURE__ */ new Set(["$", "(", ")", "+", ".", "/", "@", "\\", "^", "|"]);
424
+ var globToRegExp = (glob) => {
425
+ let source = "";
426
+ for (let at = 0; at < glob.length; at += 1) {
427
+ const character = glob[at] ?? "";
428
+ if (character === "*") {
429
+ if (glob[at + 1] === "*") {
430
+ if (glob[at + 2] === "/") {
431
+ source += "(?:[^/]*/)*";
432
+ at += 2;
433
+ } else {
434
+ source += ".*";
435
+ at += 1;
436
+ }
437
+ } else {
438
+ source += "[^/]*";
439
+ }
440
+ continue;
441
+ }
442
+ if (character === "?") {
443
+ source += "[^/]";
444
+ continue;
445
+ }
446
+ if (character === "{") {
447
+ source += "(?:";
448
+ continue;
449
+ }
450
+ if (character === "}") {
451
+ source += ")";
452
+ continue;
453
+ }
454
+ if (character === ",") {
455
+ source += "|";
456
+ continue;
457
+ }
458
+ source += SPECIAL.has(character) ? `\\${character}` : character;
459
+ }
460
+ return new RegExp(`^${source}$`);
461
+ };
462
+ var matchesGlob = (glob, path) => {
463
+ const here = globToRegExp(glob);
464
+ const name = path.slice(path.lastIndexOf("/") + 1);
465
+ return here.test(path) || !glob.includes("/") && here.test(name);
466
+ };
467
+ var layersOf = (config, rule) => {
468
+ const layers = [];
469
+ if (rule in config.rules) layers.push({ files: [], level: config.rules[rule] });
470
+ for (const override of config.overrides ?? []) {
471
+ if (rule in override.rules) layers.push({ files: override.files, level: override.rules[rule] });
472
+ }
473
+ return layers;
474
+ };
475
+ var governing = (layers, path) => {
476
+ let found;
477
+ for (const layer of layers) {
478
+ if (layer.files.length === 0 || layer.files.some((glob) => matchesGlob(glob, path))) {
479
+ found = layer;
480
+ }
481
+ }
482
+ return found;
483
+ };
484
+
485
+ // src/drift.ts
378
486
  var WORKFLOWS = ".github/workflows";
379
487
  var SETTINGS = ".claude/settings.json";
380
488
  var GEONOSIS = "geonosis.json";
381
489
  var LAW = "CLAUDE.md";
382
490
  var CEILING = 200;
383
491
  var SWITCHED_OFF = /^\s*if:\s*(?:\$\{\{\s*)?false\b/m;
384
- var TEST_FILE = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
385
492
  var finding3 = (subject, verdict, message) => ({
386
493
  check: "drift",
387
494
  message,
@@ -427,7 +534,7 @@ var holds = (parent, child) => child === parent || child.startsWith(`${parent}${
427
534
  var ownersOf = (path, workspaces) => workspaces.filter((one) => holds(one.dir, path)).toSorted((a, b) => b.dir.length - a.dir.length);
428
535
  var orphanTests = (root, workspaces) => {
429
536
  const orphaned = /* @__PURE__ */ new Map();
430
- for (const path of filesUnder(root, (name) => TEST_FILE.test(name))) {
537
+ for (const path of testFilesUnder(root)) {
431
538
  const owners = ownersOf(path, workspaces);
432
539
  if (owners.some((one) => typeof one.manifest.scripts?.test === "string")) continue;
433
540
  const owner = owners[0];
@@ -446,6 +553,170 @@ var orphanTests = (root, workspaces) => {
446
553
  )
447
554
  );
448
555
  };
556
+ var RUNS_A_FILE = /* @__PURE__ */ new Set(["bun", "node", "tsx"]);
557
+ var BETWEEN_COMMANDS = /(?:&&|\|\||[;|&()])/;
558
+ var NAMES_A_FILE = /\.[cm]?[jt]sx?$/;
559
+ var SHELL_WOULD_REWRITE = /[$*?{}]/;
560
+ var pathRunBy = (segment) => {
561
+ const words = segment.trim().split(/\s+/).filter((word) => word !== "");
562
+ for (const [index, word] of words.entries()) {
563
+ if (!RUNS_A_FILE.has(word)) continue;
564
+ const argument = words.slice(index + 1).find((one) => !one.startsWith("-"));
565
+ if (argument === void 0 || SHELL_WOULD_REWRITE.test(argument)) continue;
566
+ if (NAMES_A_FILE.test(argument)) return argument;
567
+ }
568
+ return void 0;
569
+ };
570
+ var pathsRunByScript = (script) => script.split(BETWEEN_COMMANDS).map(pathRunBy).filter((one) => one !== void 0);
571
+ var scriptPaths = (workspaces) => {
572
+ const missing = workspaces.flatMap(
573
+ (one) => Object.entries(one.manifest.scripts ?? {}).flatMap(
574
+ ([name, script]) => pathsRunByScript(script).filter((path) => !existsSync3(join4(one.dir, path))).map((path) => ({ name, path, workspace: one }))
575
+ )
576
+ );
577
+ if (missing.length === 0) {
578
+ return [
579
+ finding3("script paths", "OK", "every file a script hands to bun, node or tsx is on disk")
580
+ ];
581
+ }
582
+ return missing.map(
583
+ ({ name, path, workspace }) => finding3(
584
+ workspace.relative === "" ? "package.json" : `${workspace.relative}/package.json`,
585
+ "FAIL",
586
+ `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`
587
+ )
588
+ );
589
+ };
590
+ var workspaceBins = (workspaces) => {
591
+ const found = /* @__PURE__ */ new Map();
592
+ for (const one of workspaces) {
593
+ const declared = one.manifest.bin;
594
+ if (typeof declared === "string") {
595
+ const name = one.manifest.name;
596
+ if (name !== void 0) found.set(name.replace(/^@[^/]+\//, ""), one);
597
+ continue;
598
+ }
599
+ for (const name of Object.keys(declared ?? {})) found.set(name, one);
600
+ }
601
+ return found;
602
+ };
603
+ var linkedBins = (root, workspaces) => {
604
+ const bins = workspaceBins(workspaces);
605
+ if (bins.size === 0) {
606
+ return [finding3("workspace bins", "SKIP", "no workspace here declares a bin")];
607
+ }
608
+ const missing = workspaces.flatMap(
609
+ (one) => Object.entries(one.manifest.scripts ?? {}).flatMap(
610
+ ([script, body]) => [...new Set(body.split(/[\s;|&()]+/).filter((word) => bins.has(word)))].filter((name) => !existsSync3(join4(root, "node_modules/.bin", name))).map((name) => ({ name, script, workspace: one }))
611
+ )
612
+ );
613
+ if (missing.length === 0) {
614
+ return [
615
+ finding3(
616
+ "workspace bins",
617
+ "OK",
618
+ "every workspace bin a script names is linked under the root"
619
+ )
620
+ ];
621
+ }
622
+ return missing.map(
623
+ ({ name, script, workspace }) => finding3(
624
+ name,
625
+ "FAIL",
626
+ `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`
627
+ )
628
+ );
629
+ };
630
+ var SHELLS_TO_PNPM = /(?:^|[\s;&|(])pnpm(?:\s|$)/;
631
+ var NODE_OPTIONS = /(?:^|[\s;&|(])NODE_OPTIONS=/;
632
+ var preloadAroundPnpm = (root, workspaces) => {
633
+ const counters = (readRatchet(root)?.counters ?? []).filter(
634
+ (entry) => typeof entry.command === "string" && SHELLS_TO_PNPM.test(entry.command)
635
+ );
636
+ if (counters.length === 0) return [];
637
+ const scripts = workspaces.flatMap(
638
+ (one) => Object.entries(one.manifest.scripts ?? {}).filter(([, body]) => NODE_OPTIONS.test(body)).map(([name]) => one.relative === "" ? name : `${one.relative}:${name}`)
639
+ );
640
+ if (scripts.length === 0) return [];
641
+ const named2 = counters.map((entry) => typeof entry.key === "string" ? entry.key : String(entry.counter)).join(", ");
642
+ const some = scripts.slice(0, 3).join(", ");
643
+ const rest = scripts.length > 3 ? ` and ${scripts.length - 3} more` : "";
644
+ return [
645
+ finding3(
646
+ "NODE_OPTIONS",
647
+ "WARN",
648
+ `${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`
649
+ )
650
+ ];
651
+ };
652
+ var GATED_BY = /geonosis:gated-by:\s*(.+?)\s*(?:-->|\*\/|$)/m;
653
+ var GENERATED_BY = /Generated by\s+`([^`]+)`/m;
654
+ var CAN_CARRY_A_MARKER = /\.(?:mdc?|markdown|ya?ml|toml|[cm]?[jt]sx?|json[c5]?|txt|sh|mjs|cjs)$/i;
655
+ var HEAD_BYTES = 4096;
656
+ var markerIn = (path) => {
657
+ let handle;
658
+ try {
659
+ handle = openSync(path, "r");
660
+ } catch {
661
+ return void 0;
662
+ }
663
+ try {
664
+ const buffer = Buffer.alloc(HEAD_BYTES);
665
+ const read = readSync(handle, buffer, 0, HEAD_BYTES, 0);
666
+ const head = buffer.toString("utf8", 0, read);
667
+ const gate = GATED_BY.exec(head)?.[1];
668
+ if (gate !== void 0) return { gate };
669
+ const writer = GENERATED_BY.exec(head)?.[1];
670
+ return writer === void 0 ? void 0 : { writer };
671
+ } catch {
672
+ return void 0;
673
+ } finally {
674
+ closeSync(handle);
675
+ }
676
+ };
677
+ var everythingRun = (root, workspaces) => {
678
+ const scripts = workspaces.flatMap((one) => Object.values(one.manifest.scripts ?? {}));
679
+ const counters = (readRatchet(root)?.counters ?? []).map((entry) => String(entry.command ?? ""));
680
+ const workflows = filesUnder(
681
+ join4(root, WORKFLOWS),
682
+ (name) => name.endsWith(".yml") || name.endsWith(".yaml")
683
+ ).map((path) => {
684
+ try {
685
+ return readFileSync4(path, "utf8");
686
+ } catch {
687
+ return "";
688
+ }
689
+ });
690
+ return [...scripts, ...counters, ...workflows].join("\n");
691
+ };
692
+ var generatedFiles = (root, workspaces) => {
693
+ const marked = filesUnder(root, (name) => CAN_CARRY_A_MARKER.test(name)).map((path) => ({ marker: markerIn(path), path })).filter((one) => one.marker !== void 0);
694
+ if (marked.length === 0) {
695
+ return [
696
+ finding3(
697
+ "generated files",
698
+ "SKIP",
699
+ "nothing here says it was generated, so there is no write half here to look for a read half of"
700
+ )
701
+ ];
702
+ }
703
+ const run = everythingRun(root, workspaces);
704
+ return marked.map(({ marker, path }) => {
705
+ const at = relativePath(root, path);
706
+ if (!("gate" in marker)) {
707
+ return finding3(
708
+ at,
709
+ "FAIL",
710
+ `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`
711
+ );
712
+ }
713
+ return run.includes(marker.gate) ? finding3(at, "OK", `generated, and \`${marker.gate}\` reads it back`) : finding3(
714
+ at,
715
+ "FAIL",
716
+ `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`
717
+ );
718
+ });
719
+ };
449
720
  var readGeonosis = (root) => {
450
721
  const path = join4(root, GEONOSIS);
451
722
  if (!existsSync3(path)) return void 0;
@@ -458,13 +729,22 @@ var readGeonosis = (root) => {
458
729
  var law = (root, config) => {
459
730
  const declared = config?.law ?? {};
460
731
  const file = typeof declared.file === "string" ? declared.file : LAW;
461
- const ceiling = typeof declared.maxLines === "number" ? declared.maxLines : CEILING;
462
732
  const path = join4(root, file);
463
733
  if (!existsSync3(path)) {
464
734
  return [finding3(file, "SKIP", "there is no law file here to measure")];
465
735
  }
466
736
  const source = readFileSync4(path, "utf8");
467
737
  const lines = source.split("\n").length - (source.endsWith("\n") ? 1 : 0);
738
+ if (typeof declared.maxLines !== "number") {
739
+ return [
740
+ finding3(
741
+ file,
742
+ "SKIP",
743
+ `${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)`
744
+ )
745
+ ];
746
+ }
747
+ const ceiling = declared.maxLines;
468
748
  return [
469
749
  lines > ceiling ? finding3(
470
750
  file,
@@ -473,20 +753,40 @@ var law = (root, config) => {
473
753
  ) : finding3(file, "OK", `${lines} lines, under the ceiling of ${ceiling}`)
474
754
  ];
475
755
  };
476
- var hooks = (root) => {
756
+ var KIT_PLUGIN = "geonosis";
757
+ var enablesKit = (path) => {
758
+ if (!existsSync3(path)) return false;
759
+ try {
760
+ const parsed = JSON.parse(readFileSync4(path, "utf8"));
761
+ const enabled = parsed.enabledPlugins;
762
+ if (typeof enabled !== "object" || enabled === null) return false;
763
+ return Object.entries(enabled).some(
764
+ ([id, on]) => on !== false && id.split("@")[0] === KIT_PLUGIN
765
+ );
766
+ } catch {
767
+ return false;
768
+ }
769
+ };
770
+ var hooks = (root, userSettings) => {
477
771
  const path = join4(root, SETTINGS);
478
- if (!existsSync3(path)) {
772
+ if (enablesKit(path)) {
773
+ return [finding3(SETTINGS, "OK", `\`enabledPlugins\` here enables the kit\u2019s plugin`)];
774
+ }
775
+ if (enablesKit(userSettings)) {
479
776
  return [
480
777
  finding3(
481
778
  SETTINGS,
482
- "WARN",
483
- "nothing here installs the kit\u2019s plugin, so none of the hooks, agents or skills reach this repo \u2014 the gates run, the method does not"
779
+ "OK",
780
+ `installed elsewhere \u2014 not this repo\u2019s to declare: \`enabledPlugins\` in ${userSettings} enables it for every repo on this machine`
484
781
  )
485
782
  ];
486
783
  }
487
- const source = readFileSync4(path, "utf8");
488
784
  return [
489
- source.includes("geonosis") ? finding3(SETTINGS, "OK", "it installs the kit\u2019s plugin") : finding3(SETTINGS, "WARN", "it names no geonosis plugin or marketplace")
785
+ finding3(
786
+ SETTINGS,
787
+ "WARN",
788
+ `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}`
789
+ )
490
790
  ];
491
791
  };
492
792
  var READERS = {
@@ -504,7 +804,17 @@ var resolves = (root, name) => {
504
804
  return false;
505
805
  }
506
806
  };
507
- var blocks = (root, config, readers) => {
807
+ var declaredAnywhere = (workspaces) => new Set(
808
+ workspaces.flatMap(
809
+ (one) => [
810
+ one.manifest.dependencies,
811
+ one.manifest.devDependencies,
812
+ one.manifest.optionalDependencies,
813
+ one.manifest.peerDependencies
814
+ ].flatMap((field) => Object.keys(field ?? {}))
815
+ )
816
+ );
817
+ var blocks = (root, config, readers, workspaces) => {
508
818
  if (config === void 0) {
509
819
  return [
510
820
  finding3(
@@ -514,19 +824,30 @@ var blocks = (root, config, readers) => {
514
824
  )
515
825
  ];
516
826
  }
827
+ const declaredPackages = declaredAnywhere(workspaces);
517
828
  return Object.entries(readers).flatMap(([block, name]) => {
518
829
  const declared = config[block] !== void 0;
830
+ const chosen = declaredPackages.has(name);
519
831
  const installed = resolves(root, name);
832
+ if (declared && !chosen) {
833
+ return [
834
+ finding3(
835
+ name,
836
+ "WARN",
837
+ `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" : ""}`
838
+ )
839
+ ];
840
+ }
520
841
  if (declared && !installed) {
521
842
  return [
522
843
  finding3(
523
844
  name,
524
845
  "WARN",
525
- `geonosis.json has a "${block}" block and ${name} is not installed here \u2014 nothing reads it`
846
+ `geonosis.json has a "${block}" block, a manifest declares ${name}, and it does not resolve here \u2014 run the install`
526
847
  )
527
848
  ];
528
849
  }
529
- if (!declared && installed) {
850
+ if (!declared && chosen && installed) {
530
851
  return [
531
852
  finding3(
532
853
  name,
@@ -535,29 +856,54 @@ var blocks = (root, config, readers) => {
535
856
  )
536
857
  ];
537
858
  }
859
+ if (!declared && installed) {
860
+ return [
861
+ finding3(
862
+ name,
863
+ "OK",
864
+ `${name} is here but no manifest declares it \u2014 a transitive dependency this repo did not choose, so it wants no "${block}" block`
865
+ )
866
+ ];
867
+ }
538
868
  return declared ? [finding3(name, "OK", `a "${block}" block, and ${name} to read it`)] : [];
539
869
  });
540
870
  };
541
- var pluginDirsConfigured = (root) => {
871
+ var PLUGIN_DIR_RULE = "biological-architecture/no-unregistered-plugin-dir";
872
+ var registryList = (value) => {
873
+ if (typeof value === "string") return [value];
874
+ if (Array.isArray(value) && value.every((one) => typeof one === "string")) return value;
875
+ return void 0;
876
+ };
877
+ var pluginDirsOf = (level) => {
878
+ const options = Array.isArray(level) ? level[1] : void 0;
879
+ const registry = registryList(options?.registry);
880
+ if (options?.roots === void 0 || registry === void 0) return void 0;
881
+ return { manifests: options.manifests ?? ["index.ts"], registry, roots: options.roots };
882
+ };
883
+ var pluginDirLayers = (root) => {
542
884
  const path = join4(root, ".oxlintrc.json");
543
- if (!existsSync3(path)) return void 0;
544
- try {
545
- const parsed = JSON.parse(readFileSync4(path, "utf8"));
546
- const rule = parsed.rules?.["biological-architecture/no-unregistered-plugin-dir"];
547
- const options = Array.isArray(rule) ? rule[1] : void 0;
548
- if (options?.roots === void 0 || typeof options.registry !== "string") return void 0;
549
- return {
550
- manifests: options.manifests ?? ["index.ts"],
551
- registry: options.registry,
552
- roots: options.roots
553
- };
554
- } catch {
555
- return void 0;
885
+ if (!existsSync3(path)) return [];
886
+ const layers = [];
887
+ for (const layer of layersOf(readConfig(path, root), PLUGIN_DIR_RULE)) {
888
+ const dirs = pluginDirsOf(layer.level);
889
+ if (dirs !== void 0) layers.push({ dirs, files: layer.files });
556
890
  }
891
+ return layers;
892
+ };
893
+ var layerOver = (layers, root, relative) => {
894
+ const at = join4(root, relative);
895
+ const inside = readdirSync2(at, { withFileTypes: true }).filter((entry) => entry.isFile()).map((entry) => `${relative}/${entry.name}`);
896
+ const paths = inside.length === 0 ? [relative] : inside;
897
+ const wrapped = layers.map((layer) => ({ files: layer.files, level: layer.dirs }));
898
+ for (const path of paths) {
899
+ const found = governing(wrapped, path);
900
+ if (found !== void 0) return found.level;
901
+ }
902
+ return void 0;
557
903
  };
558
904
  var pluginDirs = (root) => {
559
- const configured = pluginDirsConfigured(root);
560
- if (configured === void 0) {
905
+ const layers = pluginDirLayers(root);
906
+ if (layers.length === 0) {
561
907
  return [
562
908
  finding3(
563
909
  "plugin directories",
@@ -566,50 +912,180 @@ var pluginDirs = (root) => {
566
912
  )
567
913
  ];
568
914
  }
569
- const registryPath = join4(root, configured.registry);
570
- if (!existsSync3(registryPath)) {
571
- return [finding3(configured.registry, "FAIL", "the registry the rule names is not there")];
915
+ const missing = [...new Set(layers.flatMap((layer) => layer.dirs.registry))].filter(
916
+ (registry) => !existsSync3(join4(root, registry))
917
+ );
918
+ if (missing.length > 0) {
919
+ return missing.map(
920
+ (registry) => finding3(registry, "FAIL", "the registry the rule names is not there")
921
+ );
572
922
  }
573
- const registry = readFileSync4(registryPath, "utf8");
574
- const unreachable = [];
575
- for (const rootDir of configured.roots) {
923
+ const source = /* @__PURE__ */ new Map();
924
+ const unreachable = /* @__PURE__ */ new Map();
925
+ for (const rootDir of new Set(layers.flatMap((layer) => layer.dirs.roots))) {
576
926
  const at = join4(root, rootDir);
577
927
  if (!existsSync3(at)) continue;
578
928
  for (const entry of readdirSync2(at, { withFileTypes: true })) {
579
929
  if (!entry.isDirectory()) continue;
580
- const hasManifest = configured.manifests.some(
581
- (name) => existsSync3(join4(at, entry.name, name))
930
+ const relative = `${rootDir}/${entry.name}`;
931
+ const governs = layerOver(layers, root, relative);
932
+ if (governs === void 0) continue;
933
+ const key = governs.registry.join(", ");
934
+ const registry = source.get(key) ?? governs.registry.map((half) => readFileSync4(join4(root, half), "utf8")).join("\n");
935
+ source.set(key, registry);
936
+ const hasManifest = governs.manifests.some((name) => existsSync3(join4(at, entry.name, name)));
937
+ if (!hasManifest || registry.includes(entry.name)) continue;
938
+ unreachable.set(key, [...unreachable.get(key) ?? [], relative]);
939
+ }
940
+ }
941
+ return [...source.keys()].toSorted().map((registry) => {
942
+ const named2 = unreachable.get(registry) ?? [];
943
+ return named2.length === 0 ? finding3(registry, "OK", "every directory under the declared roots is named by it") : finding3(
944
+ registry,
945
+ "FAIL",
946
+ `${named2.length} directory(ies) it never names: ${named2.join(", ")}`
947
+ );
948
+ });
949
+ };
950
+ var WORKSPACE_YAML = "pnpm-workspace.yaml";
951
+ var HOIST_KEY = "publicHoistPattern";
952
+ var HOIST_REPAIR = "rm -rf node_modules/.modules.yaml node_modules/.pnpm-workspace-state-v1.json && pnpm install";
953
+ var hoistPatterns = (root) => {
954
+ const path = join4(root, WORKSPACE_YAML);
955
+ if (!existsSync3(path)) return void 0;
956
+ const lines = readFileSync4(path, "utf8").split("\n");
957
+ const at = lines.findIndex((line) => new RegExp(`^${HOIST_KEY}\\s*:`).test(line));
958
+ if (at < 0) return void 0;
959
+ const patterns = [];
960
+ for (const line of lines.slice(at + 1)) {
961
+ if (/^\s*(?:#.*)?$/.test(line)) continue;
962
+ const item = /^\s+-\s*(.+?)\s*$/.exec(line);
963
+ if (item?.[1] === void 0) break;
964
+ patterns.push(item[1].replace(/^['"]|['"]$/g, ""));
965
+ }
966
+ return patterns;
967
+ };
968
+ var matching = (pattern) => new RegExp(`^${pattern.replaceAll(/[.+?^${}()|[\]\\]/g, "\\$&").replaceAll("*", ".*")}$`);
969
+ var publicHoists = (root, workspaces) => {
970
+ const patterns = hoistPatterns(root);
971
+ if (patterns === void 0) {
972
+ return [
973
+ finding3(
974
+ WORKSPACE_YAML,
975
+ "SKIP",
976
+ `no ${HOIST_KEY} is declared here, so nothing claims a package should be linked at the root`
977
+ )
978
+ ];
979
+ }
980
+ const named2 = workspaces.flatMap(
981
+ (one) => one.dir === root || one.manifest.name === void 0 ? [] : [one.manifest.name]
982
+ );
983
+ return patterns.map((pattern) => {
984
+ const shape = matching(pattern);
985
+ const hoisted = named2.filter((name) => shape.test(name));
986
+ if (hoisted.length === 0) {
987
+ return finding3(
988
+ pattern,
989
+ "SKIP",
990
+ `this ${HOIST_KEY} matches no workspace package here, so what it should have linked at the root is a question this cannot answer`
582
991
  );
583
- if (hasManifest && !registry.includes(entry.name)) {
584
- unreachable.push(`${rootDir}/${entry.name}`);
992
+ }
993
+ const pruned = hoisted.filter((name) => !existsSync3(join4(root, "node_modules", name)));
994
+ if (pruned.length === 0) {
995
+ return finding3(
996
+ pattern,
997
+ "OK",
998
+ `every workspace package this ${HOIST_KEY} matches is linked at the root`
999
+ );
1000
+ }
1001
+ return finding3(
1002
+ pattern,
1003
+ "FAIL",
1004
+ `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}`
1005
+ );
1006
+ });
1007
+ };
1008
+ var HOOK_FILES = ["lefthook.yml", "lefthook.yaml", ".lefthook.yml", ".lefthook.yaml"];
1009
+ var HOOK_DIRS = [".husky", ".githooks"];
1010
+ var VERSION_MANAGED = /(?:^|[\s;&|(])(?<runner>pnpm|npx|bunx|yarn)\b(?:\s+(?:exec|run|dlx|x))?\s+(?<bin>geonosis(?:-[a-z-]+)?)\b/;
1011
+ var NAMES_A_BIN = /(?:^|[\s;&|(/])geonosis(?:-[a-z-]+)?\b/;
1012
+ var hookLines = (root) => {
1013
+ const found = [];
1014
+ const read = (path) => {
1015
+ const at = relativePath(root, path);
1016
+ for (const line of readFileSync4(path, "utf8").split("\n")) {
1017
+ if (line.trim() !== "") found.push({ at, line });
1018
+ }
1019
+ };
1020
+ for (const name of HOOK_FILES) {
1021
+ const path = join4(root, name);
1022
+ if (existsSync3(path)) read(path);
1023
+ }
1024
+ for (const name of HOOK_DIRS) {
1025
+ const dir = join4(root, name);
1026
+ if (!existsSync3(dir)) continue;
1027
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
1028
+ if (entry.isFile() && !entry.name.startsWith("_") && !entry.name.startsWith(".")) {
1029
+ read(join4(dir, entry.name));
585
1030
  }
586
1031
  }
587
1032
  }
588
- return [
589
- unreachable.length === 0 ? finding3(
590
- configured.registry,
591
- "OK",
592
- "every directory under the declared roots is named by it"
593
- ) : finding3(
594
- configured.registry,
595
- "FAIL",
596
- `${unreachable.length} directory(ies) it never names: ${unreachable.join(", ")}`
597
- )
598
- ];
1033
+ return found;
1034
+ };
1035
+ var gitHooks = (root) => {
1036
+ const lines = hookLines(root);
1037
+ if (lines.length === 0) {
1038
+ return [finding3("git hooks", "SKIP", "no lefthook, husky or .githooks file here to read")];
1039
+ }
1040
+ const files = [...new Set(lines.map((one) => one.at))].toSorted();
1041
+ return files.flatMap((at) => {
1042
+ const here = lines.filter((one) => one.at === at);
1043
+ const named2 = here.filter((one) => NAMES_A_BIN.test(one.line));
1044
+ if (named2.length === 0) {
1045
+ return [finding3(at, "SKIP", "it names no geonosis bin, so there is nothing here to start")];
1046
+ }
1047
+ const wrong = named2.flatMap((one) => {
1048
+ const found = VERSION_MANAGED.exec(one.line)?.groups;
1049
+ return found === void 0 ? [] : [{ bin: found["bin"] ?? "", runner: found["runner"] ?? "" }];
1050
+ });
1051
+ if (wrong.length === 0) {
1052
+ return [
1053
+ finding3(
1054
+ at,
1055
+ "OK",
1056
+ `${named2.length} geonosis bin(s) here, every one called directly rather than through a runner`
1057
+ )
1058
+ ];
1059
+ }
1060
+ return wrong.map(
1061
+ ({ bin, runner }) => finding3(
1062
+ at,
1063
+ "FAIL",
1064
+ `it runs ${bin} through ${runner} \u2014 a git hook's PATH is not the shell's and carries no version-manager shim, so ${runner} fails to START and what the author reads is its message, not this gate's. Call it directly: node_modules/.bin/${bin}`
1065
+ )
1066
+ );
1067
+ });
599
1068
  };
600
1069
  var checkDrift = ({
601
1070
  readers = READERS,
602
1071
  root,
1072
+ userSettings = join4(homedir(), SETTINGS),
603
1073
  workspaces
604
1074
  }) => {
605
1075
  const config = readGeonosis(root);
606
1076
  return [
607
1077
  ...ci(root),
608
1078
  ...orphanTests(root, workspaces),
1079
+ ...scriptPaths(workspaces),
1080
+ ...linkedBins(root, workspaces),
1081
+ ...preloadAroundPnpm(root, workspaces),
1082
+ ...generatedFiles(root, workspaces),
609
1083
  ...pluginDirs(root),
1084
+ ...publicHoists(root, workspaces),
1085
+ ...gitHooks(root),
610
1086
  ...law(root, config),
611
- ...hooks(root),
612
- ...blocks(root, config, readers)
1087
+ ...hooks(root, userSettings),
1088
+ ...blocks(root, config, readers, workspaces)
613
1089
  ];
614
1090
  };
615
1091
 
@@ -672,7 +1148,38 @@ var ownReach = ({
672
1148
  };
673
1149
  };
674
1150
  var optionsOf = (level) => Array.isArray(level) ? level.slice(1) : [];
675
- var under = (config, rule) => `${rule} under ${JSON.stringify(optionsOf(config.rules[rule]))}`;
1151
+ var optionSetsOf = (config, rule) => {
1152
+ const seen = /* @__PURE__ */ new Set();
1153
+ const sets = [];
1154
+ for (const layer of layersOf(config, rule)) {
1155
+ const options = [...optionsOf(layer.level)];
1156
+ const key = JSON.stringify(options);
1157
+ if (seen.has(key)) continue;
1158
+ seen.add(key);
1159
+ sets.push(options);
1160
+ }
1161
+ return sets;
1162
+ };
1163
+ var enablingLayersOf = (config, rule) => {
1164
+ const seen = /* @__PURE__ */ new Set();
1165
+ const layers = [];
1166
+ for (const layer of layersOf(config, rule)) {
1167
+ if (OFF.has(severityOf(layer.level))) continue;
1168
+ const options = [...optionsOf(layer.level)];
1169
+ const key = JSON.stringify([layer.files, options]);
1170
+ if (seen.has(key)) continue;
1171
+ seen.add(key);
1172
+ layers.push({ files: layer.files, options });
1173
+ }
1174
+ return layers;
1175
+ };
1176
+ var under = (config, rule) => `${rule} under ${optionSetsOf(config, rule).map((options) => JSON.stringify(options)).join(" / ")}`;
1177
+ var enabledHere = (config, plugin) => [
1178
+ .../* @__PURE__ */ new Set([
1179
+ ...enabledRulesOf(config.rules, plugin),
1180
+ ...config.overrides.flatMap((one) => enabledRulesOf(one.rules, plugin))
1181
+ ])
1182
+ ].toSorted();
676
1183
  var throughProbes = ({
677
1184
  config,
678
1185
  corpus,
@@ -681,6 +1188,7 @@ var throughProbes = ({
681
1188
  silent
682
1189
  }) => {
683
1190
  const refused = /* @__PURE__ */ new Map();
1191
+ const placed = /* @__PURE__ */ new Set();
684
1192
  const dir = mkdtempSync(join5(tmpdir(), "geonosis-doctor-probe-"));
685
1193
  const here = join5(dir, "corpus");
686
1194
  try {
@@ -689,10 +1197,29 @@ var throughProbes = ({
689
1197
  const write = probes[rule];
690
1198
  if (write === void 0) continue;
691
1199
  try {
692
- const probe = write(optionsOf(config.rules[rule]));
693
- const at = join5(here, probe.path);
694
- mkdirSync(dirname2(at), { recursive: true });
695
- writeFileSync(at, probe.source);
1200
+ const layers = enablingLayersOf(config, rule);
1201
+ const unclaimed = [];
1202
+ let anyPlaced = false;
1203
+ for (const layer of layers) {
1204
+ const files = write(layer.options);
1205
+ if (files.length === 0) throw new Error("its probe declares no file at all");
1206
+ const claims = layer.files.length === 0 || files.some((file) => layer.files.some((glob) => matchesGlob(glob, file.path)));
1207
+ if (!claims) {
1208
+ unclaimed.push(
1209
+ `its probe lands at ${files[0]?.path ?? ""}, which the entry enabling it claims none of: ${layer.files.join(", ")}`
1210
+ );
1211
+ continue;
1212
+ }
1213
+ for (const file of files) {
1214
+ const at = join5(here, file.path);
1215
+ if (placed.has(at)) continue;
1216
+ placed.add(at);
1217
+ mkdirSync(dirname2(at), { recursive: true });
1218
+ writeFileSync(at, file.source);
1219
+ }
1220
+ anyPlaced = true;
1221
+ }
1222
+ if (!anyPlaced) throw new Error(unclaimed.join(" \xB7 "));
696
1223
  } catch (error) {
697
1224
  refused.set(rule, String(error.message));
698
1225
  }
@@ -735,7 +1262,7 @@ var checkExercised = async ({
735
1262
  } catch (error) {
736
1263
  return said("SKIP", refusal(error));
737
1264
  }
738
- const enabled = enabledRulesOf(config.rules, manifest.plugin);
1265
+ const enabled = enabledHere(config, manifest.plugin);
739
1266
  if (enabled.length === 0) return said("SKIP", `no ${manifest.plugin} rule is enabled here`);
740
1267
  let reach;
741
1268
  try {
@@ -796,7 +1323,7 @@ var checkExercised = async ({
796
1323
  }
797
1324
  const unplaceable = [
798
1325
  ...unprobed.map(
799
- (rule) => `${rule} declares no probe, so ${optionsOf(config.rules[rule]).length === 0 ? "its scope" : JSON.stringify(optionsOf(config.rules[rule]))} does not reach the corpus`
1326
+ (rule) => `${rule} declares no probe, so ${optionSetsOf(config, rule).every((options) => options.length === 0) ? "its scope" : under(config, rule)} does not reach the corpus`
800
1327
  ),
801
1328
  ...inert.map((rule) => `${rule}: ${probed.refused.get(rule) ?? ""}`)
802
1329
  ];
@@ -919,7 +1446,7 @@ var oneConfig = async ({
919
1446
  }
920
1447
  return held ? said("OK", `loaded ${loaded.version} = declared ${declared.spec} (${declared.at})`) : said(
921
1448
  "FAIL",
922
- `loaded ${loaded.version}, declared ${declared.spec} (${declared.at}) \u2014 a nested copy at ${loaded.at}`
1449
+ `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`
923
1450
  );
924
1451
  };
925
1452
  var labelOf = (workspace) => workspace.relative === "" ? "root" : workspace.relative;
@@ -1199,6 +1726,12 @@ var checkRunner = ({
1199
1726
  `"${script}" runs neither vitest nor bun test \u2014 this check has nothing to say about it`
1200
1727
  );
1201
1728
  }
1729
+ if (testFilesUnder(workspace.dir).length === 0) {
1730
+ return said(
1731
+ "SKIP",
1732
+ `"${script}" runs a test runner and there is no test file under this workspace \u2014 nothing here for its exit code to be wrong about`
1733
+ );
1734
+ }
1202
1735
  if (WRITES_A_REPORT.test(script)) {
1203
1736
  return said("OK", "the script asks the runner for its own JSON report, not for a status code");
1204
1737
  }
@@ -1276,28 +1809,42 @@ var countsOf = (findings) => findings.reduce((counts, one) => ({ ...counts, [one
1276
1809
  });
1277
1810
  var runDoctor = async ({
1278
1811
  baseline,
1812
+ only,
1279
1813
  oxlint,
1280
1814
  root,
1281
1815
  strict = false
1282
1816
  }) => {
1283
1817
  const configs = discoverConfigs(root);
1284
1818
  const workspaces = discoverWorkspaces(root);
1285
- const binary = oxlint ?? resolveOxlint(root);
1286
- const repoCorpus = repoCorpusOf(root);
1287
- const found = ordered([
1288
- ...await checkLoaded({ configs, root, workspaces }),
1289
- ...await exercisedOf({
1290
- configs,
1291
- oxlint: binary,
1292
- ...repoCorpus === void 0 ? {} : { repoCorpus },
1293
- root
1294
- }),
1295
- ...baselineOf({ baseline, root }),
1296
- ...checkRunner({ ratchet: readRatchet(root), workspaces }),
1297
- ...await checkObservability({ now: Date.now(), root }),
1298
- ...checkDrift({ root, workspaces }),
1299
- ...checkDeployed({ root })
1300
- ]);
1819
+ if (workspaces.length === 0 && configs.length === 0) {
1820
+ throw new DoctorError(
1821
+ `nothing here to examine \u2014 no package.json and no ${CONFIG_FILE} anywhere under ${root}. Every line this would print is about a gate that is not here, and a page of SKIPs is not a pass.`
1822
+ );
1823
+ }
1824
+ const asked = (check) => only === void 0 || only.includes(check);
1825
+ const runs = [
1826
+ ["loaded", () => checkLoaded({ configs, root, workspaces })],
1827
+ [
1828
+ "exercised",
1829
+ () => {
1830
+ const repoCorpus = repoCorpusOf(root);
1831
+ return exercisedOf({
1832
+ configs,
1833
+ oxlint: oxlint ?? resolveOxlint(root),
1834
+ ...repoCorpus === void 0 ? {} : { repoCorpus },
1835
+ root
1836
+ });
1837
+ }
1838
+ ],
1839
+ ["baseline", () => baselineOf({ baseline, root })],
1840
+ ["runner", () => checkRunner({ ratchet: readRatchet(root), workspaces })],
1841
+ ["observability", () => checkObservability({ now: Date.now(), root })],
1842
+ ["drift", () => checkDrift({ root, workspaces })],
1843
+ ["deployed", () => checkDeployed({ root })]
1844
+ ];
1845
+ const collected = [];
1846
+ for (const [check, run] of runs) if (asked(check)) collected.push(...await run());
1847
+ const found = ordered(collected);
1301
1848
  const findings = strict ? found.map((one) => one.verdict === "WARN" ? { ...one, verdict: "FAIL" } : one) : found;
1302
1849
  return {
1303
1850
  counts: countsOf(findings),
@@ -1330,13 +1877,17 @@ var sectionOf = (check, findings) => {
1330
1877
  var formatDoctor = ({ counts, findings, ok, root }) => [
1331
1878
  `geonosis-doctor \u2014 ${root}`,
1332
1879
  "",
1333
- ...CHECKS.flatMap((check) => sectionOf(check, findings)),
1334
- `${CHECKS.length} checks, ${findings.length} lines: ${counts.OK} ok, ${counts.WARN} warned, ${counts.SKIP} skipped, ${counts.UNJUDGED} unjudged, ${counts.FAIL} failed`,
1880
+ // A check nobody asked for (--only) said nothing; a section for it would read as a check that ran.
1881
+ ...CHECKS.filter((check) => findings.some((one) => one.check === check)).flatMap(
1882
+ (check) => sectionOf(check, findings)
1883
+ ),
1884
+ `${ranOf(findings)} ${ranOf(findings) === 1 ? "check" : "checks"}, ${findings.length} lines: ${counts.OK} ok, ${counts.WARN} warned, ${counts.SKIP} skipped, ${counts.UNJUDGED} unjudged, ${counts.FAIL} failed`,
1335
1885
  ok ? "doctor PASS \u2014 nothing here says the gates are measuring something other than what they claim." : "doctor FAIL \u2014 a line above is a gate reporting on something other than what it names, or a question this could not ask at all.",
1336
1886
  ""
1337
1887
  ].join("\n");
1338
1888
  var formatJson = (report) => `${JSON.stringify(report, null, 2)}
1339
1889
  `;
1890
+ var ranOf = (findings) => new Set(findings.map((one) => one.check)).size;
1340
1891
 
1341
1892
  export {
1342
1893
  CONFIG_FILE,