@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.
- package/README.md +69 -7
- package/dist/{chunk-ZJCUTDQW.js → chunk-R4OLFNI6.js} +805 -125
- package/dist/doctor-cli.js +81 -34
- package/dist/index.d.ts +41 -15
- package/dist/index.js +7 -1
- package/package.json +2 -2
|
@@ -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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
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
|
-
|
|
361
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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
|
-
"
|
|
483
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
const
|
|
547
|
-
|
|
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
|
|
560
|
-
if (
|
|
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
|
|
570
|
-
|
|
571
|
-
|
|
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
|
|
574
|
-
const unreachable =
|
|
575
|
-
for (const rootDir of
|
|
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
|
|
581
|
-
|
|
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
|
-
|
|
584
|
-
|
|
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
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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(
|
|
1200
|
+
const dir = mkdtempSync(join6(tmpdir(), "geonosis-doctor-why-"));
|
|
639
1201
|
try {
|
|
640
|
-
const probe =
|
|
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
|
|
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
|
|
685
|
-
const
|
|
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
|
|
693
|
-
const
|
|
694
|
-
|
|
695
|
-
|
|
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
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
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) =>
|
|
720
|
-
if (repoCorpus !== void 0 && !
|
|
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 (!
|
|
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 =
|
|
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
|
|
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
|
-
|
|
799
|
-
|
|
800
|
-
),
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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
|
|
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) =>
|
|
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
|
|
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 ?
|
|
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(
|
|
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
|
|
982
|
-
import { join as
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
1733
|
+
record = JSON.parse(readFileSync7(join7(root, file), "utf8"));
|
|
1070
1734
|
} catch (error) {
|
|
1071
|
-
return
|
|
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
|
|
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 ?
|
|
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
|
-
) :
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1147
|
-
import { join as
|
|
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 =
|
|
1151
|
-
if (!
|
|
1814
|
+
const path = join8(root, GEONOSIS_FILE2);
|
|
1815
|
+
if (!existsSync6(path)) return void 0;
|
|
1152
1816
|
try {
|
|
1153
|
-
const parsed = JSON.parse(
|
|
1817
|
+
const parsed = JSON.parse(readFileSync8(path, "utf8"));
|
|
1154
1818
|
const declared = parsed.doctor?.corpus;
|
|
1155
|
-
return typeof declared === "string" && declared !== "" ?
|
|
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
|
|
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
|
-
|
|
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,
|