@geonosis/doctor 1.2.0 → 1.4.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 {
@@ -308,6 +325,7 @@ var CHECKS = [
308
325
  "exercised",
309
326
  "baseline",
310
327
  "runner",
328
+ "envelope",
311
329
  "drift",
312
330
  "observability",
313
331
  "deployed"
@@ -324,7 +342,28 @@ import { existsSync as existsSync2, readFileSync as readFileSync3, realpathSync
324
342
  import { createRequire } from "module";
325
343
  import { dirname, join as join3 } from "path";
326
344
  import { pathToFileURL } from "url";
327
- var resolveFrom = (dir, specifier) => createRequire(join3(dir, "noop.js")).resolve(specifier);
345
+ var packageNameOf = (specifier) => {
346
+ const parts2 = specifier.split("/");
347
+ return specifier.startsWith("@") ? parts2.slice(0, 2).join("/") : parts2[0] ?? specifier;
348
+ };
349
+ var resolveFrom = (dir, specifier) => {
350
+ if (specifier.startsWith(".") || specifier.startsWith("/")) {
351
+ return createRequire(join3(dir, "noop.js")).resolve(specifier);
352
+ }
353
+ const name = packageNameOf(specifier);
354
+ let at = dir;
355
+ for (; ; ) {
356
+ if (existsSync2(join3(at, "node_modules", name, "package.json"))) {
357
+ return createRequire(join3(at, "noop.js")).resolve(specifier);
358
+ }
359
+ const parent = dirname(at);
360
+ if (parent === at) break;
361
+ at = parent;
362
+ }
363
+ throw new DoctorError(
364
+ `${specifier} does not resolve from ${dir} \u2014 no node_modules on the way up carries ${name}`
365
+ );
366
+ };
328
367
  var packageDirOf = (entry, name) => {
329
368
  let dir = dirname(entry);
330
369
  for (; ; ) {
@@ -353,13 +392,17 @@ var pluginVersionOf = async (entry) => {
353
392
  }
354
393
  return version;
355
394
  };
395
+ var filesOf = (answer) => Array.isArray(answer.files) ? answer.files : [answer];
356
396
  var probesOf = async (entry, plugin) => {
357
397
  const loaded = await import(pathToFileURL(entry).href);
358
398
  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
- ])
399
+ Object.entries(loaded.default?.rules ?? {}).filter(([, rule]) => typeof rule?.probe === "function").map(([name, rule]) => {
400
+ const declared = rule.probe;
401
+ return [
402
+ `${plugin}/${name}`,
403
+ (options) => filesOf(declared(options))
404
+ ];
405
+ })
363
406
  );
364
407
  };
365
408
  var corpusOfPlugin = (from, specifier) => join3(packageDirOf(resolveFrom(from, specifier), specifier), "corpus");
@@ -373,15 +416,80 @@ var real = (path) => {
373
416
  var relativeToRoot = (root, path) => relativePath(real(root), real(path));
374
417
 
375
418
  // src/drift.ts
376
- import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
419
+ import { closeSync, existsSync as existsSync3, openSync, readdirSync as readdirSync2, readFileSync as readFileSync4, readSync } from "fs";
420
+ import { homedir } from "os";
377
421
  import { join as join4, sep as sep2 } from "path";
422
+
423
+ // src/overrides.ts
424
+ var SPECIAL = /* @__PURE__ */ new Set(["$", "(", ")", "+", ".", "/", "@", "\\", "^", "|"]);
425
+ var globToRegExp = (glob) => {
426
+ let source = "";
427
+ for (let at = 0; at < glob.length; at += 1) {
428
+ const character = glob[at] ?? "";
429
+ if (character === "*") {
430
+ if (glob[at + 1] === "*") {
431
+ if (glob[at + 2] === "/") {
432
+ source += "(?:[^/]*/)*";
433
+ at += 2;
434
+ } else {
435
+ source += ".*";
436
+ at += 1;
437
+ }
438
+ } else {
439
+ source += "[^/]*";
440
+ }
441
+ continue;
442
+ }
443
+ if (character === "?") {
444
+ source += "[^/]";
445
+ continue;
446
+ }
447
+ if (character === "{") {
448
+ source += "(?:";
449
+ continue;
450
+ }
451
+ if (character === "}") {
452
+ source += ")";
453
+ continue;
454
+ }
455
+ if (character === ",") {
456
+ source += "|";
457
+ continue;
458
+ }
459
+ source += SPECIAL.has(character) ? `\\${character}` : character;
460
+ }
461
+ return new RegExp(`^${source}$`);
462
+ };
463
+ var matchesGlob = (glob, path) => {
464
+ const here = globToRegExp(glob);
465
+ const name = path.slice(path.lastIndexOf("/") + 1);
466
+ return here.test(path) || !glob.includes("/") && here.test(name);
467
+ };
468
+ var layersOf = (config, rule) => {
469
+ const layers = [];
470
+ if (rule in config.rules) layers.push({ files: [], level: config.rules[rule] });
471
+ for (const override of config.overrides ?? []) {
472
+ if (rule in override.rules) layers.push({ files: override.files, level: override.rules[rule] });
473
+ }
474
+ return layers;
475
+ };
476
+ var governing = (layers, path) => {
477
+ let found;
478
+ for (const layer of layers) {
479
+ if (layer.files.length === 0 || layer.files.some((glob) => matchesGlob(glob, path))) {
480
+ found = layer;
481
+ }
482
+ }
483
+ return found;
484
+ };
485
+
486
+ // src/drift.ts
378
487
  var WORKFLOWS = ".github/workflows";
379
488
  var SETTINGS = ".claude/settings.json";
380
489
  var GEONOSIS = "geonosis.json";
381
490
  var LAW = "CLAUDE.md";
382
491
  var CEILING = 200;
383
492
  var SWITCHED_OFF = /^\s*if:\s*(?:\$\{\{\s*)?false\b/m;
384
- var TEST_FILE = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
385
493
  var finding3 = (subject, verdict, message) => ({
386
494
  check: "drift",
387
495
  message,
@@ -427,7 +535,7 @@ var holds = (parent, child) => child === parent || child.startsWith(`${parent}${
427
535
  var ownersOf = (path, workspaces) => workspaces.filter((one) => holds(one.dir, path)).toSorted((a, b) => b.dir.length - a.dir.length);
428
536
  var orphanTests = (root, workspaces) => {
429
537
  const orphaned = /* @__PURE__ */ new Map();
430
- for (const path of filesUnder(root, (name) => TEST_FILE.test(name))) {
538
+ for (const path of testFilesUnder(root)) {
431
539
  const owners = ownersOf(path, workspaces);
432
540
  if (owners.some((one) => typeof one.manifest.scripts?.test === "string")) continue;
433
541
  const owner = owners[0];
@@ -446,6 +554,170 @@ var orphanTests = (root, workspaces) => {
446
554
  )
447
555
  );
448
556
  };
557
+ var RUNS_A_FILE = /* @__PURE__ */ new Set(["bun", "node", "tsx"]);
558
+ var BETWEEN_COMMANDS = /(?:&&|\|\||[;|&()])/;
559
+ var NAMES_A_FILE = /\.[cm]?[jt]sx?$/;
560
+ var SHELL_WOULD_REWRITE = /[$*?{}]/;
561
+ var pathRunBy = (segment) => {
562
+ const words = segment.trim().split(/\s+/).filter((word) => word !== "");
563
+ for (const [index, word] of words.entries()) {
564
+ if (!RUNS_A_FILE.has(word)) continue;
565
+ const argument = words.slice(index + 1).find((one) => !one.startsWith("-"));
566
+ if (argument === void 0 || SHELL_WOULD_REWRITE.test(argument)) continue;
567
+ if (NAMES_A_FILE.test(argument)) return argument;
568
+ }
569
+ return void 0;
570
+ };
571
+ var pathsRunByScript = (script) => script.split(BETWEEN_COMMANDS).map(pathRunBy).filter((one) => one !== void 0);
572
+ var scriptPaths = (workspaces) => {
573
+ const missing = workspaces.flatMap(
574
+ (one) => Object.entries(one.manifest.scripts ?? {}).flatMap(
575
+ ([name, script]) => pathsRunByScript(script).filter((path) => !existsSync3(join4(one.dir, path))).map((path) => ({ name, path, workspace: one }))
576
+ )
577
+ );
578
+ if (missing.length === 0) {
579
+ return [
580
+ finding3("script paths", "OK", "every file a script hands to bun, node or tsx is on disk")
581
+ ];
582
+ }
583
+ return missing.map(
584
+ ({ name, path, workspace }) => finding3(
585
+ workspace.relative === "" ? "package.json" : `${workspace.relative}/package.json`,
586
+ "FAIL",
587
+ `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`
588
+ )
589
+ );
590
+ };
591
+ var workspaceBins = (workspaces) => {
592
+ const found = /* @__PURE__ */ new Map();
593
+ for (const one of workspaces) {
594
+ const declared = one.manifest.bin;
595
+ if (typeof declared === "string") {
596
+ const name = one.manifest.name;
597
+ if (name !== void 0) found.set(name.replace(/^@[^/]+\//, ""), one);
598
+ continue;
599
+ }
600
+ for (const name of Object.keys(declared ?? {})) found.set(name, one);
601
+ }
602
+ return found;
603
+ };
604
+ var linkedBins = (root, workspaces) => {
605
+ const bins = workspaceBins(workspaces);
606
+ if (bins.size === 0) {
607
+ return [finding3("workspace bins", "SKIP", "no workspace here declares a bin")];
608
+ }
609
+ const missing = workspaces.flatMap(
610
+ (one) => Object.entries(one.manifest.scripts ?? {}).flatMap(
611
+ ([script, body]) => [...new Set(body.split(/[\s;|&()]+/).filter((word) => bins.has(word)))].filter((name) => !existsSync3(join4(root, "node_modules/.bin", name))).map((name) => ({ name, script, workspace: one }))
612
+ )
613
+ );
614
+ if (missing.length === 0) {
615
+ return [
616
+ finding3(
617
+ "workspace bins",
618
+ "OK",
619
+ "every workspace bin a script names is linked under the root"
620
+ )
621
+ ];
622
+ }
623
+ return missing.map(
624
+ ({ name, script, workspace }) => finding3(
625
+ name,
626
+ "FAIL",
627
+ `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`
628
+ )
629
+ );
630
+ };
631
+ var SHELLS_TO_PNPM = /(?:^|[\s;&|(])pnpm(?:\s|$)/;
632
+ var NODE_OPTIONS = /(?:^|[\s;&|(])NODE_OPTIONS=/;
633
+ var preloadAroundPnpm = (root, workspaces) => {
634
+ const counters = (readRatchet(root)?.counters ?? []).filter(
635
+ (entry) => typeof entry.command === "string" && SHELLS_TO_PNPM.test(entry.command)
636
+ );
637
+ if (counters.length === 0) return [];
638
+ const scripts = workspaces.flatMap(
639
+ (one) => Object.entries(one.manifest.scripts ?? {}).filter(([, body]) => NODE_OPTIONS.test(body)).map(([name]) => one.relative === "" ? name : `${one.relative}:${name}`)
640
+ );
641
+ if (scripts.length === 0) return [];
642
+ const named2 = counters.map((entry) => typeof entry.key === "string" ? entry.key : String(entry.counter)).join(", ");
643
+ const some = scripts.slice(0, 3).join(", ");
644
+ const rest = scripts.length > 3 ? ` and ${scripts.length - 3} more` : "";
645
+ return [
646
+ finding3(
647
+ "NODE_OPTIONS",
648
+ "WARN",
649
+ `${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`
650
+ )
651
+ ];
652
+ };
653
+ var GATED_BY = /geonosis:gated-by:\s*(.+?)\s*(?:-->|\*\/|$)/m;
654
+ var GENERATED_BY = /Generated by\s+`([^`]+)`/m;
655
+ var CAN_CARRY_A_MARKER = /\.(?:mdc?|markdown|ya?ml|toml|[cm]?[jt]sx?|json[c5]?|txt|sh|mjs|cjs)$/i;
656
+ var HEAD_BYTES = 4096;
657
+ var markerIn = (path) => {
658
+ let handle;
659
+ try {
660
+ handle = openSync(path, "r");
661
+ } catch {
662
+ return void 0;
663
+ }
664
+ try {
665
+ const buffer = Buffer.alloc(HEAD_BYTES);
666
+ const read = readSync(handle, buffer, 0, HEAD_BYTES, 0);
667
+ const head = buffer.toString("utf8", 0, read);
668
+ const gate = GATED_BY.exec(head)?.[1];
669
+ if (gate !== void 0) return { gate };
670
+ const writer = GENERATED_BY.exec(head)?.[1];
671
+ return writer === void 0 ? void 0 : { writer };
672
+ } catch {
673
+ return void 0;
674
+ } finally {
675
+ closeSync(handle);
676
+ }
677
+ };
678
+ var everythingRun = (root, workspaces) => {
679
+ const scripts = workspaces.flatMap((one) => Object.values(one.manifest.scripts ?? {}));
680
+ const counters = (readRatchet(root)?.counters ?? []).map((entry) => String(entry.command ?? ""));
681
+ const workflows = filesUnder(
682
+ join4(root, WORKFLOWS),
683
+ (name) => name.endsWith(".yml") || name.endsWith(".yaml")
684
+ ).map((path) => {
685
+ try {
686
+ return readFileSync4(path, "utf8");
687
+ } catch {
688
+ return "";
689
+ }
690
+ });
691
+ return [...scripts, ...counters, ...workflows].join("\n");
692
+ };
693
+ var generatedFiles = (root, workspaces) => {
694
+ const marked = filesUnder(root, (name) => CAN_CARRY_A_MARKER.test(name)).map((path) => ({ marker: markerIn(path), path })).filter((one) => one.marker !== void 0);
695
+ if (marked.length === 0) {
696
+ return [
697
+ finding3(
698
+ "generated files",
699
+ "SKIP",
700
+ "nothing here says it was generated, so there is no write half here to look for a read half of"
701
+ )
702
+ ];
703
+ }
704
+ const run = everythingRun(root, workspaces);
705
+ return marked.map(({ marker, path }) => {
706
+ const at = relativePath(root, path);
707
+ if (!("gate" in marker)) {
708
+ return finding3(
709
+ at,
710
+ "FAIL",
711
+ `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`
712
+ );
713
+ }
714
+ return run.includes(marker.gate) ? finding3(at, "OK", `generated, and \`${marker.gate}\` reads it back`) : finding3(
715
+ at,
716
+ "FAIL",
717
+ `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`
718
+ );
719
+ });
720
+ };
449
721
  var readGeonosis = (root) => {
450
722
  const path = join4(root, GEONOSIS);
451
723
  if (!existsSync3(path)) return void 0;
@@ -458,13 +730,22 @@ var readGeonosis = (root) => {
458
730
  var law = (root, config) => {
459
731
  const declared = config?.law ?? {};
460
732
  const file = typeof declared.file === "string" ? declared.file : LAW;
461
- const ceiling = typeof declared.maxLines === "number" ? declared.maxLines : CEILING;
462
733
  const path = join4(root, file);
463
734
  if (!existsSync3(path)) {
464
735
  return [finding3(file, "SKIP", "there is no law file here to measure")];
465
736
  }
466
737
  const source = readFileSync4(path, "utf8");
467
738
  const lines = source.split("\n").length - (source.endsWith("\n") ? 1 : 0);
739
+ if (typeof declared.maxLines !== "number") {
740
+ return [
741
+ finding3(
742
+ file,
743
+ "SKIP",
744
+ `${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)`
745
+ )
746
+ ];
747
+ }
748
+ const ceiling = declared.maxLines;
468
749
  return [
469
750
  lines > ceiling ? finding3(
470
751
  file,
@@ -473,20 +754,40 @@ var law = (root, config) => {
473
754
  ) : finding3(file, "OK", `${lines} lines, under the ceiling of ${ceiling}`)
474
755
  ];
475
756
  };
476
- var hooks = (root) => {
757
+ var KIT_PLUGIN = "geonosis";
758
+ var enablesKit = (path) => {
759
+ if (!existsSync3(path)) return false;
760
+ try {
761
+ const parsed = JSON.parse(readFileSync4(path, "utf8"));
762
+ const enabled = parsed.enabledPlugins;
763
+ if (typeof enabled !== "object" || enabled === null) return false;
764
+ return Object.entries(enabled).some(
765
+ ([id, on]) => on !== false && id.split("@")[0] === KIT_PLUGIN
766
+ );
767
+ } catch {
768
+ return false;
769
+ }
770
+ };
771
+ var hooks = (root, userSettings) => {
477
772
  const path = join4(root, SETTINGS);
478
- if (!existsSync3(path)) {
773
+ if (enablesKit(path)) {
774
+ return [finding3(SETTINGS, "OK", `\`enabledPlugins\` here enables the kit\u2019s plugin`)];
775
+ }
776
+ if (enablesKit(userSettings)) {
479
777
  return [
480
778
  finding3(
481
779
  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"
780
+ "OK",
781
+ `installed elsewhere \u2014 not this repo\u2019s to declare: \`enabledPlugins\` in ${userSettings} enables it for every repo on this machine`
484
782
  )
485
783
  ];
486
784
  }
487
- const source = readFileSync4(path, "utf8");
488
785
  return [
489
- source.includes("geonosis") ? finding3(SETTINGS, "OK", "it installs the kit\u2019s plugin") : finding3(SETTINGS, "WARN", "it names no geonosis plugin or marketplace")
786
+ finding3(
787
+ SETTINGS,
788
+ "WARN",
789
+ `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}`
790
+ )
490
791
  ];
491
792
  };
492
793
  var READERS = {
@@ -504,7 +805,17 @@ var resolves = (root, name) => {
504
805
  return false;
505
806
  }
506
807
  };
507
- var blocks = (root, config, readers) => {
808
+ var declaredAnywhere = (workspaces) => new Set(
809
+ workspaces.flatMap(
810
+ (one) => [
811
+ one.manifest.dependencies,
812
+ one.manifest.devDependencies,
813
+ one.manifest.optionalDependencies,
814
+ one.manifest.peerDependencies
815
+ ].flatMap((field) => Object.keys(field ?? {}))
816
+ )
817
+ );
818
+ var blocks = (root, config, readers, workspaces) => {
508
819
  if (config === void 0) {
509
820
  return [
510
821
  finding3(
@@ -514,19 +825,30 @@ var blocks = (root, config, readers) => {
514
825
  )
515
826
  ];
516
827
  }
828
+ const declaredPackages = declaredAnywhere(workspaces);
517
829
  return Object.entries(readers).flatMap(([block, name]) => {
518
830
  const declared = config[block] !== void 0;
831
+ const chosen = declaredPackages.has(name);
519
832
  const installed = resolves(root, name);
833
+ if (declared && !chosen) {
834
+ return [
835
+ finding3(
836
+ name,
837
+ "WARN",
838
+ `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" : ""}`
839
+ )
840
+ ];
841
+ }
520
842
  if (declared && !installed) {
521
843
  return [
522
844
  finding3(
523
845
  name,
524
846
  "WARN",
525
- `geonosis.json has a "${block}" block and ${name} is not installed here \u2014 nothing reads it`
847
+ `geonosis.json has a "${block}" block, a manifest declares ${name}, and it does not resolve here \u2014 run the install`
526
848
  )
527
849
  ];
528
850
  }
529
- if (!declared && installed) {
851
+ if (!declared && chosen && installed) {
530
852
  return [
531
853
  finding3(
532
854
  name,
@@ -535,29 +857,54 @@ var blocks = (root, config, readers) => {
535
857
  )
536
858
  ];
537
859
  }
860
+ if (!declared && installed) {
861
+ return [
862
+ finding3(
863
+ name,
864
+ "OK",
865
+ `${name} is here but no manifest declares it \u2014 a transitive dependency this repo did not choose, so it wants no "${block}" block`
866
+ )
867
+ ];
868
+ }
538
869
  return declared ? [finding3(name, "OK", `a "${block}" block, and ${name} to read it`)] : [];
539
870
  });
540
871
  };
541
- var pluginDirsConfigured = (root) => {
872
+ var PLUGIN_DIR_RULE = "biological-architecture/no-unregistered-plugin-dir";
873
+ var registryList = (value) => {
874
+ if (typeof value === "string") return [value];
875
+ if (Array.isArray(value) && value.every((one) => typeof one === "string")) return value;
876
+ return void 0;
877
+ };
878
+ var pluginDirsOf = (level) => {
879
+ const options = Array.isArray(level) ? level[1] : void 0;
880
+ const registry = registryList(options?.registry);
881
+ if (options?.roots === void 0 || registry === void 0) return void 0;
882
+ return { manifests: options.manifests ?? ["index.ts"], registry, roots: options.roots };
883
+ };
884
+ var pluginDirLayers = (root) => {
542
885
  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;
886
+ if (!existsSync3(path)) return [];
887
+ const layers = [];
888
+ for (const layer of layersOf(readConfig(path, root), PLUGIN_DIR_RULE)) {
889
+ const dirs = pluginDirsOf(layer.level);
890
+ if (dirs !== void 0) layers.push({ dirs, files: layer.files });
556
891
  }
892
+ return layers;
893
+ };
894
+ var layerOver = (layers, root, relative) => {
895
+ const at = join4(root, relative);
896
+ const inside = readdirSync2(at, { withFileTypes: true }).filter((entry) => entry.isFile()).map((entry) => `${relative}/${entry.name}`);
897
+ const paths = inside.length === 0 ? [relative] : inside;
898
+ const wrapped = layers.map((layer) => ({ files: layer.files, level: layer.dirs }));
899
+ for (const path of paths) {
900
+ const found = governing(wrapped, path);
901
+ if (found !== void 0) return found.level;
902
+ }
903
+ return void 0;
557
904
  };
558
905
  var pluginDirs = (root) => {
559
- const configured = pluginDirsConfigured(root);
560
- if (configured === void 0) {
906
+ const layers = pluginDirLayers(root);
907
+ if (layers.length === 0) {
561
908
  return [
562
909
  finding3(
563
910
  "plugin directories",
@@ -566,64 +913,279 @@ var pluginDirs = (root) => {
566
913
  )
567
914
  ];
568
915
  }
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")];
916
+ const missing = [...new Set(layers.flatMap((layer) => layer.dirs.registry))].filter(
917
+ (registry) => !existsSync3(join4(root, registry))
918
+ );
919
+ if (missing.length > 0) {
920
+ return missing.map(
921
+ (registry) => finding3(registry, "FAIL", "the registry the rule names is not there")
922
+ );
572
923
  }
573
- const registry = readFileSync4(registryPath, "utf8");
574
- const unreachable = [];
575
- for (const rootDir of configured.roots) {
924
+ const source = /* @__PURE__ */ new Map();
925
+ const unreachable = /* @__PURE__ */ new Map();
926
+ for (const rootDir of new Set(layers.flatMap((layer) => layer.dirs.roots))) {
576
927
  const at = join4(root, rootDir);
577
928
  if (!existsSync3(at)) continue;
578
929
  for (const entry of readdirSync2(at, { withFileTypes: true })) {
579
930
  if (!entry.isDirectory()) continue;
580
- const hasManifest = configured.manifests.some(
581
- (name) => existsSync3(join4(at, entry.name, name))
931
+ const relative = `${rootDir}/${entry.name}`;
932
+ const governs = layerOver(layers, root, relative);
933
+ if (governs === void 0) continue;
934
+ const key = governs.registry.join(", ");
935
+ const registry = source.get(key) ?? governs.registry.map((half) => readFileSync4(join4(root, half), "utf8")).join("\n");
936
+ source.set(key, registry);
937
+ const hasManifest = governs.manifests.some((name) => existsSync3(join4(at, entry.name, name)));
938
+ if (!hasManifest || registry.includes(entry.name)) continue;
939
+ unreachable.set(key, [...unreachable.get(key) ?? [], relative]);
940
+ }
941
+ }
942
+ return [...source.keys()].toSorted().map((registry) => {
943
+ const named2 = unreachable.get(registry) ?? [];
944
+ return named2.length === 0 ? finding3(registry, "OK", "every directory under the declared roots is named by it") : finding3(
945
+ registry,
946
+ "FAIL",
947
+ `${named2.length} directory(ies) it never names: ${named2.join(", ")}`
948
+ );
949
+ });
950
+ };
951
+ var WORKSPACE_YAML = "pnpm-workspace.yaml";
952
+ var HOIST_KEY = "publicHoistPattern";
953
+ var HOIST_REPAIR = "rm -rf node_modules/.modules.yaml node_modules/.pnpm-workspace-state-v1.json && pnpm install";
954
+ var hoistPatterns = (root) => {
955
+ const path = join4(root, WORKSPACE_YAML);
956
+ if (!existsSync3(path)) return void 0;
957
+ const lines = readFileSync4(path, "utf8").split("\n");
958
+ const at = lines.findIndex((line) => new RegExp(`^${HOIST_KEY}\\s*:`).test(line));
959
+ if (at < 0) return void 0;
960
+ const patterns = [];
961
+ for (const line of lines.slice(at + 1)) {
962
+ if (/^\s*(?:#.*)?$/.test(line)) continue;
963
+ const item = /^\s+-\s*(.+?)\s*$/.exec(line);
964
+ if (item?.[1] === void 0) break;
965
+ patterns.push(item[1].replace(/^['"]|['"]$/g, ""));
966
+ }
967
+ return patterns;
968
+ };
969
+ var matching = (pattern) => new RegExp(`^${pattern.replaceAll(/[.+?^${}()|[\]\\]/g, "\\$&").replaceAll("*", ".*")}$`);
970
+ var publicHoists = (root, workspaces) => {
971
+ const patterns = hoistPatterns(root);
972
+ if (patterns === void 0) {
973
+ return [
974
+ finding3(
975
+ WORKSPACE_YAML,
976
+ "SKIP",
977
+ `no ${HOIST_KEY} is declared here, so nothing claims a package should be linked at the root`
978
+ )
979
+ ];
980
+ }
981
+ const named2 = workspaces.flatMap(
982
+ (one) => one.dir === root || one.manifest.name === void 0 ? [] : [one.manifest.name]
983
+ );
984
+ return patterns.map((pattern) => {
985
+ const shape = matching(pattern);
986
+ const hoisted = named2.filter((name) => shape.test(name));
987
+ if (hoisted.length === 0) {
988
+ return finding3(
989
+ pattern,
990
+ "SKIP",
991
+ `this ${HOIST_KEY} matches no workspace package here, so what it should have linked at the root is a question this cannot answer`
582
992
  );
583
- if (hasManifest && !registry.includes(entry.name)) {
584
- unreachable.push(`${rootDir}/${entry.name}`);
993
+ }
994
+ const pruned = hoisted.filter((name) => !existsSync3(join4(root, "node_modules", name)));
995
+ if (pruned.length === 0) {
996
+ return finding3(
997
+ pattern,
998
+ "OK",
999
+ `every workspace package this ${HOIST_KEY} matches is linked at the root`
1000
+ );
1001
+ }
1002
+ return finding3(
1003
+ pattern,
1004
+ "FAIL",
1005
+ `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}`
1006
+ );
1007
+ });
1008
+ };
1009
+ var HOOK_FILES = ["lefthook.yml", "lefthook.yaml", ".lefthook.yml", ".lefthook.yaml"];
1010
+ var HOOK_DIRS = [".husky", ".githooks"];
1011
+ var VERSION_MANAGED = /(?:^|[\s;&|(])(?<runner>pnpm|npx|bunx|yarn)\b(?:\s+(?:exec|run|dlx|x))?\s+(?<bin>geonosis(?:-[a-z-]+)?)\b/;
1012
+ var NAMES_A_BIN = /(?:^|[\s;&|(/])geonosis(?:-[a-z-]+)?\b/;
1013
+ var hookLines = (root) => {
1014
+ const found = [];
1015
+ const read = (path) => {
1016
+ const at = relativePath(root, path);
1017
+ for (const line of readFileSync4(path, "utf8").split("\n")) {
1018
+ if (line.trim() !== "") found.push({ at, line });
1019
+ }
1020
+ };
1021
+ for (const name of HOOK_FILES) {
1022
+ const path = join4(root, name);
1023
+ if (existsSync3(path)) read(path);
1024
+ }
1025
+ for (const name of HOOK_DIRS) {
1026
+ const dir = join4(root, name);
1027
+ if (!existsSync3(dir)) continue;
1028
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
1029
+ if (entry.isFile() && !entry.name.startsWith("_") && !entry.name.startsWith(".")) {
1030
+ read(join4(dir, entry.name));
585
1031
  }
586
1032
  }
587
1033
  }
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
- ];
1034
+ return found;
1035
+ };
1036
+ var gitHooks = (root) => {
1037
+ const lines = hookLines(root);
1038
+ if (lines.length === 0) {
1039
+ return [finding3("git hooks", "SKIP", "no lefthook, husky or .githooks file here to read")];
1040
+ }
1041
+ const files = [...new Set(lines.map((one) => one.at))].toSorted();
1042
+ return files.flatMap((at) => {
1043
+ const here = lines.filter((one) => one.at === at);
1044
+ const named2 = here.filter((one) => NAMES_A_BIN.test(one.line));
1045
+ if (named2.length === 0) {
1046
+ return [finding3(at, "SKIP", "it names no geonosis bin, so there is nothing here to start")];
1047
+ }
1048
+ const wrong = named2.flatMap((one) => {
1049
+ const found = VERSION_MANAGED.exec(one.line)?.groups;
1050
+ return found === void 0 ? [] : [{ bin: found["bin"] ?? "", runner: found["runner"] ?? "" }];
1051
+ });
1052
+ if (wrong.length === 0) {
1053
+ return [
1054
+ finding3(
1055
+ at,
1056
+ "OK",
1057
+ `${named2.length} geonosis bin(s) here, every one called directly rather than through a runner`
1058
+ )
1059
+ ];
1060
+ }
1061
+ return wrong.map(
1062
+ ({ bin, runner }) => finding3(
1063
+ at,
1064
+ "FAIL",
1065
+ `it runs ${bin} through ${runner} \u2014 a git hook's PATH is not the shell's and carries no version-manager shim, so ${runner} fails to START and what the author reads is its message, not this gate's. Call it directly: node_modules/.bin/${bin}`
1066
+ )
1067
+ );
1068
+ });
599
1069
  };
600
1070
  var checkDrift = ({
601
1071
  readers = READERS,
602
1072
  root,
1073
+ userSettings = join4(homedir(), SETTINGS),
603
1074
  workspaces
604
1075
  }) => {
605
1076
  const config = readGeonosis(root);
606
1077
  return [
607
1078
  ...ci(root),
608
1079
  ...orphanTests(root, workspaces),
1080
+ ...scriptPaths(workspaces),
1081
+ ...linkedBins(root, workspaces),
1082
+ ...preloadAroundPnpm(root, workspaces),
1083
+ ...generatedFiles(root, workspaces),
609
1084
  ...pluginDirs(root),
1085
+ ...publicHoists(root, workspaces),
1086
+ ...gitHooks(root),
610
1087
  ...law(root, config),
611
- ...hooks(root),
612
- ...blocks(root, config, readers)
1088
+ ...hooks(root, userSettings),
1089
+ ...blocks(root, config, readers, workspaces)
613
1090
  ];
614
1091
  };
615
1092
 
1093
+ // src/envelope.ts
1094
+ import { existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync5 } from "fs";
1095
+ import { join as join5 } from "path";
1096
+ var ENVELOPES_DIR = ".geonosis/envelopes";
1097
+ var NO_ENVELOPES = "no .geonosis/envelopes/*.json \u2014 the tools write one per run, so an absent envelope is a run nobody has made here yet, and it is not a balanced one";
1098
+ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1099
+ var finding4 = (verdict, subject, message) => ({
1100
+ check: "envelope",
1101
+ message,
1102
+ subject,
1103
+ verdict
1104
+ });
1105
+ var lengthOf = (value) => Array.isArray(value) ? value.length : void 0;
1106
+ var NEXT = "Next: re-run the tool that wrote it and read the numbers it prints \u2014 a run that has lost count of its own inputs is a bug in that tool, not in this tree";
1107
+ var judge = (subject, name, parsed) => {
1108
+ if (!isRecord2(parsed)) return finding4("FAIL", subject, `is not an object. ${NEXT}`);
1109
+ const tool = parsed["tool"];
1110
+ if (typeof tool !== "string" || tool.trim() === "") {
1111
+ return finding4(
1112
+ "FAIL",
1113
+ subject,
1114
+ `names no tool, so nothing here says which run it is about. ${NEXT}`
1115
+ );
1116
+ }
1117
+ if (tool !== name) {
1118
+ return finding4(
1119
+ "FAIL",
1120
+ subject,
1121
+ `is written by "${tool}", not "${name}" \u2014 a tool writing another tool's envelope leaves both of their numbers unattributable. ${NEXT}`
1122
+ );
1123
+ }
1124
+ const version = parsed["version"];
1125
+ if (typeof version !== "string" || version.trim() === "") {
1126
+ return finding4(
1127
+ "FAIL",
1128
+ subject,
1129
+ `${tool}: names no version, so nothing dates this run and a stale envelope reads exactly like a fresh one. ${NEXT}`
1130
+ );
1131
+ }
1132
+ const considered = parsed["considered"];
1133
+ const read = parsed["read"];
1134
+ const refused = lengthOf(parsed["refused"]);
1135
+ const excused = lengthOf(parsed["excused"]);
1136
+ if (typeof considered !== "number" || typeof read !== "number" || refused === void 0 || excused === void 0) {
1137
+ return finding4(
1138
+ "FAIL",
1139
+ subject,
1140
+ `${tool}: has no considered/read/refused/excused to check \u2014 the four numbers ARE the envelope, and a file without them measures nothing. ${NEXT}`
1141
+ );
1142
+ }
1143
+ const accounted = read + refused + excused;
1144
+ return considered === accounted ? finding4(
1145
+ "OK",
1146
+ subject,
1147
+ `${tool} considered ${considered} and accounts for all of them \u2014 ${read} read + ${refused} refused + ${excused} excused`
1148
+ ) : finding4(
1149
+ "FAIL",
1150
+ subject,
1151
+ `${tool}: considered ${considered} but accounts for ${accounted} \u2014 ${read} read + ${refused} refused + ${excused} excused. It reported on fewer things than it was handed, and every verdict it printed is over the smaller number. ${NEXT}`
1152
+ );
1153
+ };
1154
+ var checkEnvelopes = ({ root }) => {
1155
+ const dir = join5(root, ENVELOPES_DIR);
1156
+ const files = existsSync4(dir) ? readdirSync3(dir).filter((name) => name.endsWith(".json")).toSorted() : [];
1157
+ if (files.length === 0) return [finding4("SKIP", ENVELOPES_DIR, NO_ENVELOPES)];
1158
+ return files.map((name) => {
1159
+ const subject = `${ENVELOPES_DIR}/${name}`;
1160
+ let parsed;
1161
+ try {
1162
+ parsed = JSON.parse(readFileSync5(join5(dir, name), "utf8"));
1163
+ } catch (error) {
1164
+ return finding4("FAIL", subject, `is not readable JSON: ${error.message}. ${NEXT}`);
1165
+ }
1166
+ return judge(subject, name.replace(/\.json$/, ""), parsed);
1167
+ });
1168
+ };
1169
+
616
1170
  // src/exercised.ts
617
1171
  import { spawnSync as spawnSync2 } from "child_process";
618
- import { cpSync, existsSync as existsSync4, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs";
1172
+ import {
1173
+ cpSync,
1174
+ existsSync as existsSync5,
1175
+ mkdirSync,
1176
+ mkdtempSync,
1177
+ readFileSync as readFileSync6,
1178
+ rmSync,
1179
+ writeFileSync
1180
+ } from "fs";
619
1181
  import { tmpdir } from "os";
620
- import { dirname as dirname2, join as join5 } from "path";
1182
+ import { dirname as dirname2, join as join6, resolve } from "path";
621
1183
  import { corpusOf, readManifest } from "@geonosis/lint-parity";
622
1184
  var OFF = /* @__PURE__ */ new Set([0, "0", "allow", "off", false]);
623
1185
  var severityOf = (level) => Array.isArray(level) ? level[0] : level;
624
1186
  var enabledRulesOf = (rules, plugin) => Object.keys(rules).filter((id) => id.startsWith(`${plugin}/`) && !OFF.has(severityOf(rules[id]))).toSorted();
625
1187
  var passes = (findings) => !findings.some((one) => one.verdict === "FAIL" || one.verdict === "UNJUDGED");
626
- var finding4 = (subject, verdict, message) => ({
1188
+ var finding5 = (subject, verdict, message) => ({
627
1189
  check: "exercised",
628
1190
  message,
629
1191
  subject,
@@ -635,9 +1197,9 @@ var refusal = (error) => {
635
1197
  return said.length > LIMIT ? `${said.slice(0, LIMIT)}\u2026` : said;
636
1198
  };
637
1199
  var reasonFrom = (config, oxlint) => {
638
- const dir = mkdtempSync(join5(tmpdir(), "geonosis-doctor-why-"));
1200
+ const dir = mkdtempSync(join6(tmpdir(), "geonosis-doctor-why-"));
639
1201
  try {
640
- const probe = join5(dir, "probe.tsx");
1202
+ const probe = join6(dir, "probe.tsx");
641
1203
  writeFileSync(probe, "export const probe = 1\n");
642
1204
  const run = spawnSync2(
643
1205
  oxlint,
@@ -672,7 +1234,52 @@ var ownReach = ({
672
1234
  };
673
1235
  };
674
1236
  var optionsOf = (level) => Array.isArray(level) ? level.slice(1) : [];
675
- var under = (config, rule) => `${rule} under ${JSON.stringify(optionsOf(config.rules[rule]))}`;
1237
+ var optionSetsOf = (config, rule) => {
1238
+ const seen = /* @__PURE__ */ new Set();
1239
+ const sets = [];
1240
+ for (const layer of layersOf(config, rule)) {
1241
+ const options = [...optionsOf(layer.level)];
1242
+ const key = JSON.stringify(options);
1243
+ if (seen.has(key)) continue;
1244
+ seen.add(key);
1245
+ sets.push(options);
1246
+ }
1247
+ return sets;
1248
+ };
1249
+ var enablingLayersOf = (config, rule) => {
1250
+ const seen = /* @__PURE__ */ new Set();
1251
+ const layers = [];
1252
+ for (const layer of layersOf(config, rule)) {
1253
+ if (OFF.has(severityOf(layer.level))) continue;
1254
+ const options = [...optionsOf(layer.level)];
1255
+ const key = JSON.stringify([layer.files, options]);
1256
+ if (seen.has(key)) continue;
1257
+ seen.add(key);
1258
+ layers.push({ files: layer.files, options });
1259
+ }
1260
+ return layers;
1261
+ };
1262
+ var under = (config, rule) => `${rule} under ${optionSetsOf(config, rule).map((options) => JSON.stringify(options)).join(" / ")}`;
1263
+ var enabledHere = (config, plugin) => [
1264
+ .../* @__PURE__ */ new Set([
1265
+ ...enabledRulesOf(config.rules, plugin),
1266
+ ...config.overrides.flatMap((one) => enabledRulesOf(one.rules, plugin))
1267
+ ])
1268
+ ].toSorted();
1269
+ var concreteDirOf = (glob) => {
1270
+ const segments = glob.split("/");
1271
+ const out = [];
1272
+ for (const segment of segments.slice(0, -1)) {
1273
+ if (segment === "**") continue;
1274
+ if (segment === "*") {
1275
+ out.push("geonosis");
1276
+ continue;
1277
+ }
1278
+ if (segment.includes("*") || segment.includes("{")) return void 0;
1279
+ out.push(segment);
1280
+ }
1281
+ return out.join("/");
1282
+ };
676
1283
  var throughProbes = ({
677
1284
  config,
678
1285
  corpus,
@@ -681,29 +1288,80 @@ var throughProbes = ({
681
1288
  silent
682
1289
  }) => {
683
1290
  const refused = /* @__PURE__ */ new Map();
684
- const dir = mkdtempSync(join5(tmpdir(), "geonosis-doctor-probe-"));
685
- const here = join5(dir, "corpus");
1291
+ const placed = /* @__PURE__ */ new Set();
1292
+ const dir = mkdtempSync(join6(tmpdir(), "geonosis-doctor-probe-"));
1293
+ const here = join6(dir, "corpus");
686
1294
  try {
687
1295
  cpSync(corpus, here, { recursive: true });
688
1296
  for (const rule of silent) {
689
1297
  const write = probes[rule];
690
1298
  if (write === void 0) continue;
691
1299
  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);
1300
+ const layers = enablingLayersOf(config, rule);
1301
+ const unclaimed = [];
1302
+ let anyPlaced = false;
1303
+ for (const layer of layers) {
1304
+ const files = write(layer.options);
1305
+ if (files.length === 0) throw new Error("its probe declares no file at all");
1306
+ const claims = layer.files.length === 0 || files.some((file) => layer.files.some((glob) => matchesGlob(glob, file.path)));
1307
+ let mounted = files;
1308
+ if (!claims) {
1309
+ const rehomed = layer.files.map((glob) => {
1310
+ const home = concreteDirOf(glob);
1311
+ if (home === void 0) return void 0;
1312
+ const extension = /^\*(\.[\w.]+)$/.exec(glob.split("/").at(-1) ?? "")?.[1];
1313
+ const moved = files.map((file) => {
1314
+ const base = file.path.split("/").at(-1) ?? file.path;
1315
+ const adapted = extension === void 0 || base.endsWith(extension) ? base : base.replace(/\.[^.]+$/, extension);
1316
+ const kept = layer.files.some((one) => matchesGlob(one, `${home}/${base}`));
1317
+ return { ...file, path: `${home}/${kept ? base : adapted}` };
1318
+ });
1319
+ return moved.every((file) => layer.files.some((one) => matchesGlob(one, file.path))) ? moved : void 0;
1320
+ }).find((moved) => moved !== void 0);
1321
+ if (rehomed === void 0) {
1322
+ unclaimed.push(
1323
+ `its probe lands at ${files[0]?.path ?? ""}, which the entry enabling it claims none of (and no claimed path could be synthesized): ${layer.files.join(", ")}`
1324
+ );
1325
+ continue;
1326
+ }
1327
+ mounted = rehomed;
1328
+ }
1329
+ for (const file of mounted) {
1330
+ const at2 = join6(here, file.path);
1331
+ if (placed.has(at2)) continue;
1332
+ placed.add(at2);
1333
+ mkdirSync(dirname2(at2), { recursive: true });
1334
+ writeFileSync(at2, file.source);
1335
+ }
1336
+ anyPlaced = true;
1337
+ }
1338
+ if (!anyPlaced) throw new Error(unclaimed.join(" \xB7 "));
696
1339
  } catch (error) {
697
1340
  refused.set(rule, String(error.message));
698
1341
  }
699
1342
  }
700
- const reach = corpusOf({
701
- configA: config.path,
702
- configB: config.path,
703
- corpus: here,
704
- oxlint
705
- }).reach;
706
- return { fired: new Set(reach.filter((one) => one.firedInA).map((one) => one.rule)), refused };
1343
+ const probeConfig = JSON.parse(readFileSync6(config.path, "utf8"));
1344
+ const configDir = dirname2(config.path);
1345
+ probeConfig.jsPlugins = (probeConfig.jsPlugins ?? []).map(
1346
+ (spec) => spec.startsWith(".") || spec.startsWith("/") ? resolve(configDir, spec) : packageDirOf(resolveFrom(configDir, spec), spec)
1347
+ );
1348
+ const at = join6(here, ".oxlintrc-geonosis-probe.json");
1349
+ writeFileSync(at, JSON.stringify(probeConfig, null, 2));
1350
+ const run = spawnSync2(
1351
+ oxlint,
1352
+ ["-c", at, "--format", "json", "--no-ignore", "--disable-nested-config", here],
1353
+ { cwd: dir, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 }
1354
+ );
1355
+ const fired = /* @__PURE__ */ new Set();
1356
+ try {
1357
+ const parsed = JSON.parse(run.stdout || "{}");
1358
+ for (const one of parsed.diagnostics ?? []) {
1359
+ const renamed = /^([\w@./-]+)\((.+)\)$/.exec(one.code ?? "");
1360
+ if (renamed !== null) fired.add(`${renamed[1]}/${renamed[2]}`);
1361
+ }
1362
+ } catch {
1363
+ }
1364
+ return { fired, refused };
707
1365
  } finally {
708
1366
  rmSync(dir, { force: true, recursive: true });
709
1367
  }
@@ -716,14 +1374,14 @@ var checkExercised = async ({
716
1374
  repoCorpus,
717
1375
  root
718
1376
  }) => {
719
- const said = (verdict, message) => finding4(config.relative, verdict, message);
720
- if (repoCorpus !== void 0 && !existsSync4(repoCorpus)) {
1377
+ const said = (verdict, message) => finding5(config.relative, verdict, message);
1378
+ if (repoCorpus !== void 0 && !existsSync5(repoCorpus)) {
721
1379
  return said(
722
1380
  "FAIL",
723
1381
  `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`
724
1382
  );
725
1383
  }
726
- if (!existsSync4(corpus)) {
1384
+ if (!existsSync5(corpus)) {
727
1385
  return said(
728
1386
  "SKIP",
729
1387
  `the plugin loaded from here ships no corpus at ${relativeToRoot(root, corpus)} \u2014 nothing declares which rules it can be evidence about`
@@ -735,7 +1393,7 @@ var checkExercised = async ({
735
1393
  } catch (error) {
736
1394
  return said("SKIP", refusal(error));
737
1395
  }
738
- const enabled = enabledRulesOf(config.rules, manifest.plugin);
1396
+ const enabled = enabledHere(config, manifest.plugin);
739
1397
  if (enabled.length === 0) return said("SKIP", `no ${manifest.plugin} rule is enabled here`);
740
1398
  let reach;
741
1399
  try {
@@ -774,7 +1432,8 @@ var checkExercised = async ({
774
1432
  if (silent.length === 0) {
775
1433
  return said("OK", `${enabled.length} enabled, ${enabled.length} exercised${where}`);
776
1434
  }
777
- const probes = entry === void 0 ? {} : await probesOf(entry, manifest.plugin).catch(() => ({}));
1435
+ const loadedProbes = entry === void 0 ? null : await probesOf(entry, manifest.plugin).catch(() => null);
1436
+ const probes = loadedProbes ?? {};
778
1437
  const declared = silent.filter((rule) => probes[rule] !== void 0);
779
1438
  const unprobed = silent.filter((rule) => probes[rule] === void 0);
780
1439
  let probed = { fired: /* @__PURE__ */ new Set(), refused: /* @__PURE__ */ new Map() };
@@ -794,13 +1453,18 @@ var checkExercised = async ({
794
1453
  `${countOf(placed, "fire")} nowhere in the corpus and nothing through the probe each declares either: ${named(placed.map((rule) => under(config, rule)))}`
795
1454
  );
796
1455
  }
797
- const unplaceable = [
798
- ...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`
800
- ),
801
- ...inert.map((rule) => `${rule}: ${probed.refused.get(rule) ?? ""}`)
802
- ];
803
- if (unplaceable.length > 0) return said("UNJUDGED", named(unplaceable));
1456
+ const unplaceable = inert.map((rule) => `${rule}: ${probed.refused.get(rule) ?? ""}`);
1457
+ 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(", ")}`;
1458
+ const unknowable = loadedProbes === null ? unprobed.map(
1459
+ (rule) => `${rule} declares no probe, so ${optionSetsOf(config, rule).every((options) => options.length === 0) ? "its scope" : under(config, rule)} does not reach the corpus`
1460
+ ) : [];
1461
+ if (unplaceable.length > 0 || unknowable.length > 0) {
1462
+ return said(
1463
+ "UNJUDGED",
1464
+ named([...unknowable, ...unplaceable]) + (kitNote === "" ? "" : ` \u2014 and ${kitNote}`)
1465
+ );
1466
+ }
1467
+ if (kitNote !== "") return said("WARN", kitNote);
804
1468
  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`;
805
1469
  return said("OK", `${enabled.length} enabled, ${enabled.length} exercised${where} \u2014 ${through}`);
806
1470
  };
@@ -876,7 +1540,7 @@ var declaredFor = ({
876
1540
  }
877
1541
  return void 0;
878
1542
  };
879
- var finding5 = (subject, verdict, message) => ({
1543
+ var finding6 = (subject, verdict, message) => ({
880
1544
  check: "loaded",
881
1545
  message,
882
1546
  subject,
@@ -893,7 +1557,7 @@ var oneConfig = async ({
893
1557
  specifier,
894
1558
  workspaces
895
1559
  }) => {
896
- const said = (verdict, message) => finding5(config.relative, verdict, `${specifier}: ${message}`);
1560
+ const said = (verdict, message) => finding6(config.relative, verdict, `${specifier}: ${message}`);
897
1561
  let loaded;
898
1562
  try {
899
1563
  loaded = await versionAt(resolveFrom(config.dir, specifier), specifier, root);
@@ -945,10 +1609,10 @@ var copiesOf = async ({
945
1609
  found.set(at, { from: [labelOf(workspace)], version: await pluginVersionOf(entry) });
946
1610
  }
947
1611
  if (found.size === 0) {
948
- return finding5(specifier, "FAIL", "no workspace in this tree can resolve it at all");
1612
+ return finding6(specifier, "FAIL", "no workspace in this tree can resolve it at all");
949
1613
  }
950
1614
  const listed = [...found.entries()].map(([at, one]) => `${at} ${one.version} (${one.from.join(", ")})`).join("; ");
951
- return found.size === 1 ? finding5(specifier, "OK", `1 copy \u2014 ${listed}`) : finding5(
1615
+ return found.size === 1 ? finding6(specifier, "OK", `1 copy \u2014 ${listed}`) : finding6(
952
1616
  specifier,
953
1617
  "WARN",
954
1618
  `${found.size} copies \u2014 ${listed}. Which one oxlint runs depends on which directory its config sits in.`
@@ -963,7 +1627,7 @@ var checkLoaded = async ({
963
1627
  const specifiers = /* @__PURE__ */ new Set();
964
1628
  for (const config of configs) {
965
1629
  if (config.error !== void 0) {
966
- findings.push(finding5(config.relative, "FAIL", config.error));
1630
+ findings.push(finding6(config.relative, "FAIL", config.error));
967
1631
  continue;
968
1632
  }
969
1633
  for (const specifier of config.jsPlugins.filter((name) => name.startsWith(SCOPE))) {
@@ -978,13 +1642,13 @@ var checkLoaded = async ({
978
1642
  };
979
1643
 
980
1644
  // src/observability.ts
981
- import { readFileSync as readFileSync5 } from "fs";
982
- import { join as join6 } from "path";
1645
+ import { readFileSync as readFileSync7 } from "fs";
1646
+ import { join as join7 } from "path";
983
1647
  var GEONOSIS_FILE = "geonosis.json";
984
1648
  var REACHES_NOTHING = /* @__PURE__ */ new Set(["console", "memory", "noop", "none", "null", "swallowing"]);
985
1649
  var DEFAULT_MAX_AGE_SECONDS = 3600;
986
1650
  var HEAD_TIMEOUT_MS = 3e3;
987
- var finding6 = (verdict, subject, message) => ({
1651
+ var finding7 = (verdict, subject, message) => ({
988
1652
  check: "observability",
989
1653
  message,
990
1654
  subject,
@@ -993,7 +1657,7 @@ var finding6 = (verdict, subject, message) => ({
993
1657
  var readGeonosis2 = (root) => {
994
1658
  let text;
995
1659
  try {
996
- text = readFileSync5(join6(root, GEONOSIS_FILE), "utf8");
1660
+ text = readFileSync7(join7(root, GEONOSIS_FILE), "utf8");
997
1661
  } catch {
998
1662
  return { present: false };
999
1663
  }
@@ -1011,25 +1675,25 @@ var readGeonosis2 = (root) => {
1011
1675
  var exporterFinding = (config) => {
1012
1676
  const sink = config.sink;
1013
1677
  if (typeof sink !== "string" || sink.trim() === "") {
1014
- return finding6(
1678
+ return finding7(
1015
1679
  "FAIL",
1016
1680
  GEONOSIS_FILE,
1017
1681
  "observability.sink is not set, so nothing here says where errors are supposed to go \u2014 and a repo that cannot name its exporter has not got one"
1018
1682
  );
1019
1683
  }
1020
1684
  if (REACHES_NOTHING.has(sink.toLowerCase())) {
1021
- return finding6(
1685
+ return finding7(
1022
1686
  "WARN",
1023
1687
  GEONOSIS_FILE,
1024
1688
  `the configured sink is "${sink}", which answers ok and reaches nothing. Correct in a dev tree; in a deployed one it is the instrument that cannot fail.`
1025
1689
  );
1026
1690
  }
1027
- return finding6("OK", GEONOSIS_FILE, `the configured sink is "${sink}"`);
1691
+ return finding7("OK", GEONOSIS_FILE, `the configured sink is "${sink}"`);
1028
1692
  };
1029
1693
  var reachableFinding = async (config) => {
1030
1694
  const endpoint = config.endpoint;
1031
1695
  if (typeof endpoint !== "string" || endpoint.trim() === "") {
1032
- return finding6(
1696
+ return finding7(
1033
1697
  "SKIP",
1034
1698
  GEONOSIS_FILE,
1035
1699
  "no observability.endpoint was named, so whether the exporter is reachable was not asked"
@@ -1039,13 +1703,13 @@ var reachableFinding = async (config) => {
1039
1703
  const timer = setTimeout(() => controller.abort(), HEAD_TIMEOUT_MS);
1040
1704
  try {
1041
1705
  const response = await fetch(endpoint, { method: "HEAD", signal: controller.signal });
1042
- return finding6(
1706
+ return finding7(
1043
1707
  "OK",
1044
1708
  GEONOSIS_FILE,
1045
1709
  `${endpoint} is reachable \u2014 it answered ${response.status} to a HEAD`
1046
1710
  );
1047
1711
  } catch (error) {
1048
- return finding6(
1712
+ return finding7(
1049
1713
  "FAIL",
1050
1714
  GEONOSIS_FILE,
1051
1715
  `${endpoint} is not reachable from here: ${error.message}. Every report this repo sends is going into that.`
@@ -1057,7 +1721,7 @@ var reachableFinding = async (config) => {
1057
1721
  var ageFinding = (config, root, now) => {
1058
1722
  const file = config.lastEventFile;
1059
1723
  if (typeof file !== "string" || file.trim() === "") {
1060
- return finding6(
1724
+ return finding7(
1061
1725
  "SKIP",
1062
1726
  GEONOSIS_FILE,
1063
1727
  "no observability.lastEventFile was configured, so when the last event arrived is not a question anything here can answer. Have the sink write { at, id, sink } on every capture and name the file."
@@ -1066,27 +1730,27 @@ var ageFinding = (config, root, now) => {
1066
1730
  const maxAgeSeconds = typeof config.maxAgeSeconds === "number" && config.maxAgeSeconds > 0 ? config.maxAgeSeconds : DEFAULT_MAX_AGE_SECONDS;
1067
1731
  let record;
1068
1732
  try {
1069
- record = JSON.parse(readFileSync5(join6(root, file), "utf8"));
1733
+ record = JSON.parse(readFileSync7(join7(root, file), "utf8"));
1070
1734
  } catch (error) {
1071
- return finding6(
1735
+ return finding7(
1072
1736
  "FAIL",
1073
1737
  file,
1074
1738
  `the last event file could not be read: ${error.message}. A sink that has never written one has never captured anything.`
1075
1739
  );
1076
1740
  }
1077
1741
  if (typeof record.at !== "number" || !Number.isFinite(record.at)) {
1078
- return finding6(
1742
+ return finding7(
1079
1743
  "FAIL",
1080
1744
  file,
1081
1745
  'the last event record has no numeric "at", so its age cannot be read \u2014 and an age nobody can read is not an age inside the window'
1082
1746
  );
1083
1747
  }
1084
1748
  const ageSeconds = Math.round((now - record.at) / 1e3);
1085
- return ageSeconds > maxAgeSeconds ? finding6(
1749
+ return ageSeconds > maxAgeSeconds ? finding7(
1086
1750
  "FAIL",
1087
1751
  file,
1088
1752
  `the last event arrived ${ageSeconds}s ago, past the ${maxAgeSeconds}s window. An exporter that stopped, a key that was rotated and a sink that has been dropping since Tuesday all look exactly like this, and all of them leave a green build.`
1089
- ) : finding6(
1753
+ ) : finding7(
1090
1754
  "OK",
1091
1755
  file,
1092
1756
  `the last event arrived ${ageSeconds}s ago, inside the ${maxAgeSeconds}s window`
@@ -1095,16 +1759,16 @@ var ageFinding = (config, root, now) => {
1095
1759
  var probeFinding = (config) => {
1096
1760
  const probe = config.probe;
1097
1761
  if (typeof probe === "string" && probe.trim() !== "") {
1098
- return finding6("OK", GEONOSIS_FILE, `the probe that proves this exporter is "${probe}"`);
1762
+ return finding7("OK", GEONOSIS_FILE, `the probe that proves this exporter is "${probe}"`);
1099
1763
  }
1100
1764
  if (typeof config.lastEventFile === "string" && config.lastEventFile.trim() !== "") {
1101
- return finding6(
1765
+ return finding7(
1102
1766
  "OK",
1103
1767
  GEONOSIS_FILE,
1104
1768
  "no probe command, but a last event file is read above, so something does look at this exporter"
1105
1769
  );
1106
1770
  }
1107
- return finding6(
1771
+ return finding7(
1108
1772
  "WARN",
1109
1773
  GEONOSIS_FILE,
1110
1774
  "neither observability.probe nor observability.lastEventFile is configured, so nothing in this repo has ever established that a report reaches the sink. Name a probe command \u2014 the doctor reports it, your gate runs it."
@@ -1117,7 +1781,7 @@ var checkObservability = async ({
1117
1781
  const read = readGeonosis2(root);
1118
1782
  if (read.error !== void 0) {
1119
1783
  return [
1120
- finding6(
1784
+ finding7(
1121
1785
  "FAIL",
1122
1786
  GEONOSIS_FILE,
1123
1787
  `${GEONOSIS_FILE} could not be parsed: ${read.error}. A config nobody can read has not been read, and every question below would have been answered from a default nobody chose.`
@@ -1126,7 +1790,7 @@ var checkObservability = async ({
1126
1790
  }
1127
1791
  if (!read.present || read.config === void 0) {
1128
1792
  return [
1129
- finding6(
1793
+ finding7(
1130
1794
  "SKIP",
1131
1795
  GEONOSIS_FILE,
1132
1796
  `no observability block in ${GEONOSIS_FILE}, so nothing here knows where this repo sends its errors. Add { sink, endpoint, lastEventFile | probe, maxAgeSeconds } to have this asked.`
@@ -1143,16 +1807,16 @@ var checkObservability = async ({
1143
1807
  };
1144
1808
 
1145
1809
  // src/repo-corpus.ts
1146
- import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
1147
- import { join as join7 } from "path";
1810
+ import { existsSync as existsSync6, readFileSync as readFileSync8 } from "fs";
1811
+ import { join as join8 } from "path";
1148
1812
  var GEONOSIS_FILE2 = "geonosis.json";
1149
1813
  var repoCorpusOf = (root) => {
1150
- const path = join7(root, GEONOSIS_FILE2);
1151
- if (!existsSync5(path)) return void 0;
1814
+ const path = join8(root, GEONOSIS_FILE2);
1815
+ if (!existsSync6(path)) return void 0;
1152
1816
  try {
1153
- const parsed = JSON.parse(readFileSync6(path, "utf8"));
1817
+ const parsed = JSON.parse(readFileSync8(path, "utf8"));
1154
1818
  const declared = parsed.doctor?.corpus;
1155
- return typeof declared === "string" && declared !== "" ? join7(root, declared) : void 0;
1819
+ return typeof declared === "string" && declared !== "" ? join8(root, declared) : void 0;
1156
1820
  } catch {
1157
1821
  return void 0;
1158
1822
  }
@@ -1163,7 +1827,7 @@ var TEST_FAILURES = "testFailures";
1163
1827
  var RUNS_A_RUNNER = /(?:^|[\s;&|(])(?:npx\s+|bunx\s+|pnpm\s+(?:exec\s+)?)?(?:vitest|bun\s+test)(?:\s|$)/;
1164
1828
  var RUNS_BUN_TEST = /(?:^|[\s;&|(])(?:bunx\s+)?bun\s+test(?:\s|$)/;
1165
1829
  var WRITES_A_REPORT = /--reporter[= ]\S*json|--outputFile/i;
1166
- var finding7 = (subject, verdict, message) => ({
1830
+ var finding8 = (subject, verdict, message) => ({
1167
1831
  check: "runner",
1168
1832
  message,
1169
1833
  subject,
@@ -1191,7 +1855,7 @@ var checkRunner = ({
1191
1855
  if (script === "") return [];
1192
1856
  const subject = workspace.relative === "" ? "package.json" : `${workspace.relative}/package.json`;
1193
1857
  const said = (verdict, message) => [
1194
- finding7(subject, verdict, message)
1858
+ finding8(subject, verdict, message)
1195
1859
  ];
1196
1860
  if (!RUNS_A_RUNNER.test(script)) {
1197
1861
  return said(
@@ -1199,6 +1863,12 @@ var checkRunner = ({
1199
1863
  `"${script}" runs neither vitest nor bun test \u2014 this check has nothing to say about it`
1200
1864
  );
1201
1865
  }
1866
+ if (testFilesUnder(workspace.dir).length === 0) {
1867
+ return said(
1868
+ "SKIP",
1869
+ `"${script}" runs a test runner and there is no test file under this workspace \u2014 nothing here for its exit code to be wrong about`
1870
+ );
1871
+ }
1202
1872
  if (WRITES_A_REPORT.test(script)) {
1203
1873
  return said("OK", "the script asks the runner for its own JSON report, not for a status code");
1204
1874
  }
@@ -1283,6 +1953,11 @@ var runDoctor = async ({
1283
1953
  }) => {
1284
1954
  const configs = discoverConfigs(root);
1285
1955
  const workspaces = discoverWorkspaces(root);
1956
+ if (workspaces.length === 0 && configs.length === 0) {
1957
+ throw new DoctorError(
1958
+ `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.`
1959
+ );
1960
+ }
1286
1961
  const asked = (check) => only === void 0 || only.includes(check);
1287
1962
  const runs = [
1288
1963
  ["loaded", () => checkLoaded({ configs, root, workspaces })],
@@ -1300,6 +1975,7 @@ var runDoctor = async ({
1300
1975
  ],
1301
1976
  ["baseline", () => baselineOf({ baseline, root })],
1302
1977
  ["runner", () => checkRunner({ ratchet: readRatchet(root), workspaces })],
1978
+ ["envelope", () => checkEnvelopes({ root })],
1303
1979
  ["observability", () => checkObservability({ now: Date.now(), root })],
1304
1980
  ["drift", () => checkDrift({ root, workspaces })],
1305
1981
  ["deployed", () => checkDeployed({ root })]
@@ -1321,6 +1997,7 @@ var ABOUT = {
1321
1997
  baseline: "a number that may only shrink, against another ref",
1322
1998
  deployed: "what the pipeline reported deploying is what the tree declares",
1323
1999
  drift: "the gates that were set up and are no longer running",
2000
+ envelope: "every gate read as many things as it was handed",
1324
2001
  exercised: "every enabled rule fires on at least one corpus file",
1325
2002
  loaded: "the plugin oxlint would load is the one the manifest pins",
1326
2003
  observability: "an exporter is configured, reachable, and something arrived through it lately",
@@ -1374,6 +2051,9 @@ export {
1374
2051
  relativeToRoot,
1375
2052
  READERS,
1376
2053
  checkDrift,
2054
+ ENVELOPES_DIR,
2055
+ NO_ENVELOPES,
2056
+ checkEnvelopes,
1377
2057
  enabledRulesOf,
1378
2058
  checkExercised,
1379
2059
  SCOPE,