@geonosis/doctor 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -7
- package/dist/{chunk-ZJCUTDQW.js → chunk-7BRGHYQR.js} +605 -67
- package/dist/doctor-cli.js +74 -34
- package/dist/index.d.ts +14 -10
- package/dist/index.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -13,6 +13,11 @@ npx geonosis-doctor
|
|
|
13
13
|
Run it **after every bump** and **in CI**. It exits 1 when any line is a FAIL or an UNJUDGED — a
|
|
14
14
|
question it could not ask is not a pass — and 2 when the run could not be made at all.
|
|
15
15
|
|
|
16
|
+
**A directory that is not a project is a 2, not a pass.** With no `package.json` and no
|
|
17
|
+
`.oxlintrc.json` anywhere under the root there is nothing here to examine, and every line it would
|
|
18
|
+
print is about a gate that is not there. It used to answer `doctor PASS` at exit 0 off six SKIPs and
|
|
19
|
+
three vacuous OKs — from the tool whose thesis is that a SKIP is not a pass.
|
|
20
|
+
|
|
16
21
|
```
|
|
17
22
|
geonosis-doctor [--root <dir>] [--json] [--strict] [--baseline-against [<ref>]] [--oxlint <path>]
|
|
18
23
|
```
|
|
@@ -50,6 +55,26 @@ npm and pnpm alike.
|
|
|
50
55
|
A declared range this check cannot read is a **FAIL**, not a pass. A version it cannot judge is a
|
|
51
56
|
refusal.
|
|
52
57
|
|
|
58
|
+
## The options a rule actually runs under
|
|
59
|
+
|
|
60
|
+
Every reader here that consumes a rule's configuration reads `overrides[]` as well as `rules`, and
|
|
61
|
+
resolves them the way oxlint does — **measured**, not assumed, against oxlint 1.80:
|
|
62
|
+
|
|
63
|
+
- an override entry naming a rule again **replaces** its options for the files its `files` globs
|
|
64
|
+
claim; nothing is deep-merged into the base;
|
|
65
|
+
- when several overrides claim one file, the **last** of them wins outright;
|
|
66
|
+
- a rule an override names and the base block never mentions is **enabled**, on those files.
|
|
67
|
+
|
|
68
|
+
Reading `rules` alone is one config with two answers. dielime registers its Medusa providers from
|
|
69
|
+
three files — a base `medusa-config.ts` and two carrier registries — and declares that exactly
|
|
70
|
+
where oxlint reads it, in overrides naming the carrier directories. The rule resolved it; `drift`
|
|
71
|
+
read the base object and reported two working carriers as directories the registry "never names";
|
|
72
|
+
`exercised` would have called a rule enabled only in an override "not enabled here" and skipped it.
|
|
73
|
+
|
|
74
|
+
The glob subset understood is `**` across segments, `*` and `?` within one, and `{a,b}`; a pattern
|
|
75
|
+
with no separator matches a basename anywhere. Like `pathMatching` in the rule plugin, it is a
|
|
76
|
+
subset that says so rather than a dependency that would have to be right about everything.
|
|
77
|
+
|
|
53
78
|
### `exercised` — enabled ≠ exercised
|
|
54
79
|
|
|
55
80
|
A rule at `"error"` over a tree with no violation of it, and a rule at `"error"` that can never
|
|
@@ -111,10 +136,14 @@ probe derives its path from the option, never from a fixed corpus location, and
|
|
|
111
136
|
`lib/probe.ts`'s `pathMatching` builds one back out of the regex and then tests it against that
|
|
112
137
|
regex, refusing with a sentence rather than placing a file the rule will never look at.
|
|
113
138
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
139
|
+
A probe declares FILES, not a file. `no-unregistered-plugin-dir` reports a directory its registry
|
|
140
|
+
does not name and reads that registry off disk, returning silently when it is not there — so one
|
|
141
|
+
file could never exercise it, and dielime, enforcing it at error over four registries, read as a
|
|
142
|
+
permanent FAIL for a gate that was working. Its probe now declares the directory AND the registry,
|
|
143
|
+
and every file a probe declares is mounted into the copy. A probe declaring only `{ path, source }`
|
|
144
|
+
is the one-file case of the same thing and is read unchanged, which matters: this reads probes off
|
|
145
|
+
the plugin build the consumer has INSTALLED, and every build published before this shape existed
|
|
146
|
+
answers in the bare form.
|
|
118
147
|
|
|
119
148
|
#### The corpus a shipped one cannot be
|
|
120
149
|
|
|
@@ -172,6 +201,10 @@ naming bun. It used to be warned with vitest's sentence — eight lines of it in
|
|
|
172
201
|
report, each blaming a runner the check had not read, which is how a reader learns to skim the
|
|
173
202
|
section and miss the vitest lines beside them that are true.
|
|
174
203
|
|
|
204
|
+
A workspace whose test script runs a runner over **zero test files** is a SKIP, not a warning:
|
|
205
|
+
dielime's `packages/eslint-config` runs `vitest run` and has no test file under it, so there is no
|
|
206
|
+
result for an exit code to be wrong about. The message says what was counted.
|
|
207
|
+
|
|
175
208
|
The message names the runner it read. What it does not do is see through a wrapper: a script that
|
|
176
209
|
runs `bun ./scripts/vitest-strict.ts` reads as neither, and that consumer's wrapper is in fact the
|
|
177
210
|
exemplary fix — it spawns vitest with `--reporter=json`, reads the report, and fails on a missing
|
|
@@ -185,10 +218,39 @@ The other four ask whether a gate measures what it names. This one asks whether
|
|
|
185
218
|
|---|---|---|
|
|
186
219
|
| `.github/workflows/*.yml` | FAIL | a job or step switched off by a condition that can never be true. One consumer carried 90 tests, a pgvector container and an E2E suite and ran none of it, the disablement documented as "TEMPORARY" in a comment. It is "never downgrade a rule", one level up |
|
|
187
220
|
| test files | FAIL | a `*.test.*` file with no `test` script in any workspace from it up to the root. A monorepo that runs one vitest over the whole tree has no orphan in it, and this says so |
|
|
221
|
+
| script paths | FAIL | a `scripts` entry that hands `bun`, `node` or `tsx` a file that is not on disk. during.day's `bun run fast` began `bun scripts/check-plugin-version.ts && …` and the script had been deleted: the 2.5 s gate had failed at its first command for as long as nobody looked. Only a word with a file extension is asked about, so `bun test`, `bun run lint` and `node --version` are not paths |
|
|
222
|
+
| generated files | FAIL | a file carrying `geonosis:gated-by: <gate>` that nothing here runs the gate for — checked against every manifest script, every ratchet counter command and every workflow, because what matters is that SOMETHING runs it. A file that names its **writer** and no gate at all fails on that alone: during.day's AGENTS.md is a 1,117-line byte-copy of its own law, written by `geonosis sync`, and nothing ever ran `sync --check` — the doctor stood beside it warning about the shorter original |
|
|
223
|
+
| `NODE_OPTIONS` | WARN | a manifest script preloading through `NODE_OPTIONS` beside a ratchet counter whose command shells to `pnpm`. The nested pnpm inherits the option: dielime's ts5 shim sent it looking for a `.pnpmfile.mjs` that is not there, the counter died, the ratchet refused correctly, and nothing in the message was near the cause. Both halves must be present, and the line names the script and the counter |
|
|
224
|
+
| workspace bins | FAIL | a script calling a bin THIS repo's own workspaces declare, with no `node_modules/.bin/<name>` at the root. pnpm links a workspace bin only for the packages that depend on it: dielime's `@dielime/gate-tools` had to become an explicit root devDependency before the gate scripts could find it, and naming that took a debugging session. External commands are not judged — that would be a guess |
|
|
188
225
|
| plugin directories | FAIL | a manifest directory under the roots `no-unregistered-plugin-dir` names that the registry never mentions. SKIP when the rule is not configured — the roots are the repo's, read from the rule it already wrote, never a list invented here |
|
|
189
|
-
|
|
|
190
|
-
|
|
|
191
|
-
|
|
|
226
|
+
| `publicHoistPattern` | FAIL / SKIP | a pattern in `pnpm-workspace.yaml` with no root `node_modules` link for a workspace package it matches. pnpm dropped one during an **unrelated** `add`, then answered "Already up to date" to every install afterwards over a clean `git status` — the tree was fixed only by deleting `.modules.yaml` and `.pnpm-workspace-state-v1.json`, which the line carries verbatim. Only workspace packages are judged, because one exists on disk by construction and a missing link for it cannot be "the install never ran"; the root package is never asked about, since no package manager puts a repo's root inside its own `node_modules`. A pattern matching no workspace package is SKIP, never a pass |
|
|
227
|
+
| the law | WARN / SKIP | its measured length against `geonosis.json` → `law.maxLines`. **SKIP when the repo declares no ceiling** — during.day read "1,104 lines against a ceiling of 200" for a number it never chose, which is unactionable in both directions. The length is still printed, because that is the number a repo declares against |
|
|
228
|
+
| `.claude/settings.json` | WARN | nothing under `enabledPlugins` enables the kit's plugin, so the gates run and the method does not (D-041). The **user-scope** `~/.claude/settings.json` is read too and acknowledged as "installed elsewhere — not this repo's to declare": both source repos got this WARN while the plugin was installed machine-wide. The key is read, never the word `geonosis` appearing somewhere in the file |
|
|
229
|
+
| `geonosis.json` blocks | WARN | a block whose package **no manifest declares** — nothing reads it, and a stale `node_modules` copy is named as what a previous install left behind; a block whose package is declared and does not resolve — run the install; or a **declared** package with no block, running on defaults nobody chose |
|
|
230
|
+
|
|
231
|
+
The two directions do not read the same source. Whether a repo still *chose* a package is a question
|
|
232
|
+
for the **manifests**: a `release` block whose package had been removed from every manifest, with a
|
|
233
|
+
stale `node_modules` directory still present, resolved perfectly and the check said nothing (#78).
|
|
234
|
+
Whether a tool will actually load is the forward question, and that one is resolution's.
|
|
235
|
+
|
|
236
|
+
The "installed with no block" direction is scoped to **direct** dependencies. `@geonosis/review` and
|
|
237
|
+
`@geonosis/testbed` arrive transitively through `@geonosis/cli`, and asking a consumer to configure
|
|
238
|
+
a package they never installed is noise — which is how a reader learns to skip the section (#61). A
|
|
239
|
+
transitive presence gets an OK line naming where it came from, and no demand.
|
|
240
|
+
|
|
241
|
+
#### A geonosis bin a git hook cannot start
|
|
242
|
+
|
|
243
|
+
Every committed hook — `lefthook.yml`, `.husky/*`, `.githooks/*` — is read, and a line running a
|
|
244
|
+
geonosis bin through `pnpm`, `npx`, `bunx` or `yarn` is a **FAIL** naming the direct call that
|
|
245
|
+
replaces it.
|
|
246
|
+
|
|
247
|
+
A git hook's PATH is not the shell's: no nvm shim, no corepack shim, nothing a version manager put
|
|
248
|
+
there. dielime's first hooked commit was "refused" on 2026-08-30 and the ledger never ran — `pnpm`
|
|
249
|
+
was not found, and the sentence its author read was a version manager's failure wearing a gate's
|
|
250
|
+
refusal costume. That is the worst shape a gate has, because it is indistinguishable from the gate
|
|
251
|
+
working. `node_modules/.bin/<bin>` is linked by every package manager and needs nothing on PATH.
|
|
252
|
+
|
|
253
|
+
A hook file naming no geonosis bin is a **SKIP** with the sentence, never a vacuous OK.
|
|
192
254
|
|
|
193
255
|
### `observability` — a sink nobody can reach
|
|
194
256
|
|
|
@@ -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 {
|
|
@@ -324,7 +341,28 @@ import { existsSync as existsSync2, readFileSync as readFileSync3, realpathSync
|
|
|
324
341
|
import { createRequire } from "module";
|
|
325
342
|
import { dirname, join as join3 } from "path";
|
|
326
343
|
import { pathToFileURL } from "url";
|
|
327
|
-
var
|
|
344
|
+
var packageNameOf = (specifier) => {
|
|
345
|
+
const parts2 = specifier.split("/");
|
|
346
|
+
return specifier.startsWith("@") ? parts2.slice(0, 2).join("/") : parts2[0] ?? specifier;
|
|
347
|
+
};
|
|
348
|
+
var resolveFrom = (dir, specifier) => {
|
|
349
|
+
if (specifier.startsWith(".") || specifier.startsWith("/")) {
|
|
350
|
+
return createRequire(join3(dir, "noop.js")).resolve(specifier);
|
|
351
|
+
}
|
|
352
|
+
const name = packageNameOf(specifier);
|
|
353
|
+
let at = dir;
|
|
354
|
+
for (; ; ) {
|
|
355
|
+
if (existsSync2(join3(at, "node_modules", name, "package.json"))) {
|
|
356
|
+
return createRequire(join3(at, "noop.js")).resolve(specifier);
|
|
357
|
+
}
|
|
358
|
+
const parent = dirname(at);
|
|
359
|
+
if (parent === at) break;
|
|
360
|
+
at = parent;
|
|
361
|
+
}
|
|
362
|
+
throw new DoctorError(
|
|
363
|
+
`${specifier} does not resolve from ${dir} \u2014 no node_modules on the way up carries ${name}`
|
|
364
|
+
);
|
|
365
|
+
};
|
|
328
366
|
var packageDirOf = (entry, name) => {
|
|
329
367
|
let dir = dirname(entry);
|
|
330
368
|
for (; ; ) {
|
|
@@ -353,13 +391,17 @@ var pluginVersionOf = async (entry) => {
|
|
|
353
391
|
}
|
|
354
392
|
return version;
|
|
355
393
|
};
|
|
394
|
+
var filesOf = (answer) => Array.isArray(answer.files) ? answer.files : [answer];
|
|
356
395
|
var probesOf = async (entry, plugin) => {
|
|
357
396
|
const loaded = await import(pathToFileURL(entry).href);
|
|
358
397
|
return Object.fromEntries(
|
|
359
|
-
Object.entries(loaded.default?.rules ?? {}).filter(([, rule]) => typeof rule?.probe === "function").map(([name, rule]) =>
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
398
|
+
Object.entries(loaded.default?.rules ?? {}).filter(([, rule]) => typeof rule?.probe === "function").map(([name, rule]) => {
|
|
399
|
+
const declared = rule.probe;
|
|
400
|
+
return [
|
|
401
|
+
`${plugin}/${name}`,
|
|
402
|
+
(options) => filesOf(declared(options))
|
|
403
|
+
];
|
|
404
|
+
})
|
|
363
405
|
);
|
|
364
406
|
};
|
|
365
407
|
var corpusOfPlugin = (from, specifier) => join3(packageDirOf(resolveFrom(from, specifier), specifier), "corpus");
|
|
@@ -373,15 +415,80 @@ var real = (path) => {
|
|
|
373
415
|
var relativeToRoot = (root, path) => relativePath(real(root), real(path));
|
|
374
416
|
|
|
375
417
|
// src/drift.ts
|
|
376
|
-
import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
|
|
418
|
+
import { closeSync, existsSync as existsSync3, openSync, readdirSync as readdirSync2, readFileSync as readFileSync4, readSync } from "fs";
|
|
419
|
+
import { homedir } from "os";
|
|
377
420
|
import { join as join4, sep as sep2 } from "path";
|
|
421
|
+
|
|
422
|
+
// src/overrides.ts
|
|
423
|
+
var SPECIAL = /* @__PURE__ */ new Set(["$", "(", ")", "+", ".", "/", "@", "\\", "^", "|"]);
|
|
424
|
+
var globToRegExp = (glob) => {
|
|
425
|
+
let source = "";
|
|
426
|
+
for (let at = 0; at < glob.length; at += 1) {
|
|
427
|
+
const character = glob[at] ?? "";
|
|
428
|
+
if (character === "*") {
|
|
429
|
+
if (glob[at + 1] === "*") {
|
|
430
|
+
if (glob[at + 2] === "/") {
|
|
431
|
+
source += "(?:[^/]*/)*";
|
|
432
|
+
at += 2;
|
|
433
|
+
} else {
|
|
434
|
+
source += ".*";
|
|
435
|
+
at += 1;
|
|
436
|
+
}
|
|
437
|
+
} else {
|
|
438
|
+
source += "[^/]*";
|
|
439
|
+
}
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (character === "?") {
|
|
443
|
+
source += "[^/]";
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
if (character === "{") {
|
|
447
|
+
source += "(?:";
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
if (character === "}") {
|
|
451
|
+
source += ")";
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
if (character === ",") {
|
|
455
|
+
source += "|";
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
source += SPECIAL.has(character) ? `\\${character}` : character;
|
|
459
|
+
}
|
|
460
|
+
return new RegExp(`^${source}$`);
|
|
461
|
+
};
|
|
462
|
+
var matchesGlob = (glob, path) => {
|
|
463
|
+
const here = globToRegExp(glob);
|
|
464
|
+
const name = path.slice(path.lastIndexOf("/") + 1);
|
|
465
|
+
return here.test(path) || !glob.includes("/") && here.test(name);
|
|
466
|
+
};
|
|
467
|
+
var layersOf = (config, rule) => {
|
|
468
|
+
const layers = [];
|
|
469
|
+
if (rule in config.rules) layers.push({ files: [], level: config.rules[rule] });
|
|
470
|
+
for (const override of config.overrides ?? []) {
|
|
471
|
+
if (rule in override.rules) layers.push({ files: override.files, level: override.rules[rule] });
|
|
472
|
+
}
|
|
473
|
+
return layers;
|
|
474
|
+
};
|
|
475
|
+
var governing = (layers, path) => {
|
|
476
|
+
let found;
|
|
477
|
+
for (const layer of layers) {
|
|
478
|
+
if (layer.files.length === 0 || layer.files.some((glob) => matchesGlob(glob, path))) {
|
|
479
|
+
found = layer;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return found;
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
// src/drift.ts
|
|
378
486
|
var WORKFLOWS = ".github/workflows";
|
|
379
487
|
var SETTINGS = ".claude/settings.json";
|
|
380
488
|
var GEONOSIS = "geonosis.json";
|
|
381
489
|
var LAW = "CLAUDE.md";
|
|
382
490
|
var CEILING = 200;
|
|
383
491
|
var SWITCHED_OFF = /^\s*if:\s*(?:\$\{\{\s*)?false\b/m;
|
|
384
|
-
var TEST_FILE = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
385
492
|
var finding3 = (subject, verdict, message) => ({
|
|
386
493
|
check: "drift",
|
|
387
494
|
message,
|
|
@@ -427,7 +534,7 @@ var holds = (parent, child) => child === parent || child.startsWith(`${parent}${
|
|
|
427
534
|
var ownersOf = (path, workspaces) => workspaces.filter((one) => holds(one.dir, path)).toSorted((a, b) => b.dir.length - a.dir.length);
|
|
428
535
|
var orphanTests = (root, workspaces) => {
|
|
429
536
|
const orphaned = /* @__PURE__ */ new Map();
|
|
430
|
-
for (const path of
|
|
537
|
+
for (const path of testFilesUnder(root)) {
|
|
431
538
|
const owners = ownersOf(path, workspaces);
|
|
432
539
|
if (owners.some((one) => typeof one.manifest.scripts?.test === "string")) continue;
|
|
433
540
|
const owner = owners[0];
|
|
@@ -446,6 +553,170 @@ var orphanTests = (root, workspaces) => {
|
|
|
446
553
|
)
|
|
447
554
|
);
|
|
448
555
|
};
|
|
556
|
+
var RUNS_A_FILE = /* @__PURE__ */ new Set(["bun", "node", "tsx"]);
|
|
557
|
+
var BETWEEN_COMMANDS = /(?:&&|\|\||[;|&()])/;
|
|
558
|
+
var NAMES_A_FILE = /\.[cm]?[jt]sx?$/;
|
|
559
|
+
var SHELL_WOULD_REWRITE = /[$*?{}]/;
|
|
560
|
+
var pathRunBy = (segment) => {
|
|
561
|
+
const words = segment.trim().split(/\s+/).filter((word) => word !== "");
|
|
562
|
+
for (const [index, word] of words.entries()) {
|
|
563
|
+
if (!RUNS_A_FILE.has(word)) continue;
|
|
564
|
+
const argument = words.slice(index + 1).find((one) => !one.startsWith("-"));
|
|
565
|
+
if (argument === void 0 || SHELL_WOULD_REWRITE.test(argument)) continue;
|
|
566
|
+
if (NAMES_A_FILE.test(argument)) return argument;
|
|
567
|
+
}
|
|
568
|
+
return void 0;
|
|
569
|
+
};
|
|
570
|
+
var pathsRunByScript = (script) => script.split(BETWEEN_COMMANDS).map(pathRunBy).filter((one) => one !== void 0);
|
|
571
|
+
var scriptPaths = (workspaces) => {
|
|
572
|
+
const missing = workspaces.flatMap(
|
|
573
|
+
(one) => Object.entries(one.manifest.scripts ?? {}).flatMap(
|
|
574
|
+
([name, script]) => pathsRunByScript(script).filter((path) => !existsSync3(join4(one.dir, path))).map((path) => ({ name, path, workspace: one }))
|
|
575
|
+
)
|
|
576
|
+
);
|
|
577
|
+
if (missing.length === 0) {
|
|
578
|
+
return [
|
|
579
|
+
finding3("script paths", "OK", "every file a script hands to bun, node or tsx is on disk")
|
|
580
|
+
];
|
|
581
|
+
}
|
|
582
|
+
return missing.map(
|
|
583
|
+
({ name, path, workspace }) => finding3(
|
|
584
|
+
workspace.relative === "" ? "package.json" : `${workspace.relative}/package.json`,
|
|
585
|
+
"FAIL",
|
|
586
|
+
`the "${name}" script runs ${path} and there is no such file \u2014 this script fails at that command, and every gate that calls it has been running nothing since the file went`
|
|
587
|
+
)
|
|
588
|
+
);
|
|
589
|
+
};
|
|
590
|
+
var workspaceBins = (workspaces) => {
|
|
591
|
+
const found = /* @__PURE__ */ new Map();
|
|
592
|
+
for (const one of workspaces) {
|
|
593
|
+
const declared = one.manifest.bin;
|
|
594
|
+
if (typeof declared === "string") {
|
|
595
|
+
const name = one.manifest.name;
|
|
596
|
+
if (name !== void 0) found.set(name.replace(/^@[^/]+\//, ""), one);
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
for (const name of Object.keys(declared ?? {})) found.set(name, one);
|
|
600
|
+
}
|
|
601
|
+
return found;
|
|
602
|
+
};
|
|
603
|
+
var linkedBins = (root, workspaces) => {
|
|
604
|
+
const bins = workspaceBins(workspaces);
|
|
605
|
+
if (bins.size === 0) {
|
|
606
|
+
return [finding3("workspace bins", "SKIP", "no workspace here declares a bin")];
|
|
607
|
+
}
|
|
608
|
+
const missing = workspaces.flatMap(
|
|
609
|
+
(one) => Object.entries(one.manifest.scripts ?? {}).flatMap(
|
|
610
|
+
([script, body]) => [...new Set(body.split(/[\s;|&()]+/).filter((word) => bins.has(word)))].filter((name) => !existsSync3(join4(root, "node_modules/.bin", name))).map((name) => ({ name, script, workspace: one }))
|
|
611
|
+
)
|
|
612
|
+
);
|
|
613
|
+
if (missing.length === 0) {
|
|
614
|
+
return [
|
|
615
|
+
finding3(
|
|
616
|
+
"workspace bins",
|
|
617
|
+
"OK",
|
|
618
|
+
"every workspace bin a script names is linked under the root"
|
|
619
|
+
)
|
|
620
|
+
];
|
|
621
|
+
}
|
|
622
|
+
return missing.map(
|
|
623
|
+
({ name, script, workspace }) => finding3(
|
|
624
|
+
name,
|
|
625
|
+
"FAIL",
|
|
626
|
+
`the "${script}" script in ${workspace.relative === "" ? "package.json" : `${workspace.relative}/package.json`} calls it, ${bins.get(name)?.manifest.name ?? "a workspace"} declares it, and node_modules/.bin/${name} is not there \u2014 add that package as a dependency of the one whose script calls it, so the package manager links the bin`
|
|
627
|
+
)
|
|
628
|
+
);
|
|
629
|
+
};
|
|
630
|
+
var SHELLS_TO_PNPM = /(?:^|[\s;&|(])pnpm(?:\s|$)/;
|
|
631
|
+
var NODE_OPTIONS = /(?:^|[\s;&|(])NODE_OPTIONS=/;
|
|
632
|
+
var preloadAroundPnpm = (root, workspaces) => {
|
|
633
|
+
const counters = (readRatchet(root)?.counters ?? []).filter(
|
|
634
|
+
(entry) => typeof entry.command === "string" && SHELLS_TO_PNPM.test(entry.command)
|
|
635
|
+
);
|
|
636
|
+
if (counters.length === 0) return [];
|
|
637
|
+
const scripts = workspaces.flatMap(
|
|
638
|
+
(one) => Object.entries(one.manifest.scripts ?? {}).filter(([, body]) => NODE_OPTIONS.test(body)).map(([name]) => one.relative === "" ? name : `${one.relative}:${name}`)
|
|
639
|
+
);
|
|
640
|
+
if (scripts.length === 0) return [];
|
|
641
|
+
const named2 = counters.map((entry) => typeof entry.key === "string" ? entry.key : String(entry.counter)).join(", ");
|
|
642
|
+
const some = scripts.slice(0, 3).join(", ");
|
|
643
|
+
const rest = scripts.length > 3 ? ` and ${scripts.length - 3} more` : "";
|
|
644
|
+
return [
|
|
645
|
+
finding3(
|
|
646
|
+
"NODE_OPTIONS",
|
|
647
|
+
"WARN",
|
|
648
|
+
`${scripts.length} script(s) preload something through NODE_OPTIONS (${some}${rest}), and the "${named2}" counter(s) shell out to pnpm \u2014 a nested pnpm INHERITS the option and dies on it (a preloaded resolver patch sends it looking for a .pnpmfile.mjs that is not there). Run the ratchet outside those scripts, or unset NODE_OPTIONS for the nested call: env -u NODE_OPTIONS pnpm \u2026`
|
|
649
|
+
)
|
|
650
|
+
];
|
|
651
|
+
};
|
|
652
|
+
var GATED_BY = /geonosis:gated-by:\s*(.+?)\s*(?:-->|\*\/|$)/m;
|
|
653
|
+
var GENERATED_BY = /Generated by\s+`([^`]+)`/m;
|
|
654
|
+
var CAN_CARRY_A_MARKER = /\.(?:mdc?|markdown|ya?ml|toml|[cm]?[jt]sx?|json[c5]?|txt|sh|mjs|cjs)$/i;
|
|
655
|
+
var HEAD_BYTES = 4096;
|
|
656
|
+
var markerIn = (path) => {
|
|
657
|
+
let handle;
|
|
658
|
+
try {
|
|
659
|
+
handle = openSync(path, "r");
|
|
660
|
+
} catch {
|
|
661
|
+
return void 0;
|
|
662
|
+
}
|
|
663
|
+
try {
|
|
664
|
+
const buffer = Buffer.alloc(HEAD_BYTES);
|
|
665
|
+
const read = readSync(handle, buffer, 0, HEAD_BYTES, 0);
|
|
666
|
+
const head = buffer.toString("utf8", 0, read);
|
|
667
|
+
const gate = GATED_BY.exec(head)?.[1];
|
|
668
|
+
if (gate !== void 0) return { gate };
|
|
669
|
+
const writer = GENERATED_BY.exec(head)?.[1];
|
|
670
|
+
return writer === void 0 ? void 0 : { writer };
|
|
671
|
+
} catch {
|
|
672
|
+
return void 0;
|
|
673
|
+
} finally {
|
|
674
|
+
closeSync(handle);
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
var everythingRun = (root, workspaces) => {
|
|
678
|
+
const scripts = workspaces.flatMap((one) => Object.values(one.manifest.scripts ?? {}));
|
|
679
|
+
const counters = (readRatchet(root)?.counters ?? []).map((entry) => String(entry.command ?? ""));
|
|
680
|
+
const workflows = filesUnder(
|
|
681
|
+
join4(root, WORKFLOWS),
|
|
682
|
+
(name) => name.endsWith(".yml") || name.endsWith(".yaml")
|
|
683
|
+
).map((path) => {
|
|
684
|
+
try {
|
|
685
|
+
return readFileSync4(path, "utf8");
|
|
686
|
+
} catch {
|
|
687
|
+
return "";
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
return [...scripts, ...counters, ...workflows].join("\n");
|
|
691
|
+
};
|
|
692
|
+
var generatedFiles = (root, workspaces) => {
|
|
693
|
+
const marked = filesUnder(root, (name) => CAN_CARRY_A_MARKER.test(name)).map((path) => ({ marker: markerIn(path), path })).filter((one) => one.marker !== void 0);
|
|
694
|
+
if (marked.length === 0) {
|
|
695
|
+
return [
|
|
696
|
+
finding3(
|
|
697
|
+
"generated files",
|
|
698
|
+
"SKIP",
|
|
699
|
+
"nothing here says it was generated, so there is no write half here to look for a read half of"
|
|
700
|
+
)
|
|
701
|
+
];
|
|
702
|
+
}
|
|
703
|
+
const run = everythingRun(root, workspaces);
|
|
704
|
+
return marked.map(({ marker, path }) => {
|
|
705
|
+
const at = relativePath(root, path);
|
|
706
|
+
if (!("gate" in marker)) {
|
|
707
|
+
return finding3(
|
|
708
|
+
at,
|
|
709
|
+
"FAIL",
|
|
710
|
+
`generated by \`${marker.writer}\` and it names no gate that reads it back. A written file no check reads is the write half of an instrument with no read half: it can drift from its source for ever and every gate stays green. Re-run \`${marker.writer}\` on a version that writes the \`geonosis:gated-by:\` marker, and run the gate it names`
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
return run.includes(marker.gate) ? finding3(at, "OK", `generated, and \`${marker.gate}\` reads it back`) : finding3(
|
|
714
|
+
at,
|
|
715
|
+
"FAIL",
|
|
716
|
+
`generated, and nothing here runs \`${marker.gate}\` \u2014 the gate it names. A written file no check reads is the write half of an instrument with no read half: it can drift from its source for ever and every gate stays green. Add \`${marker.gate}\` to a script, a counter or a workflow`
|
|
717
|
+
);
|
|
718
|
+
});
|
|
719
|
+
};
|
|
449
720
|
var readGeonosis = (root) => {
|
|
450
721
|
const path = join4(root, GEONOSIS);
|
|
451
722
|
if (!existsSync3(path)) return void 0;
|
|
@@ -458,13 +729,22 @@ var readGeonosis = (root) => {
|
|
|
458
729
|
var law = (root, config) => {
|
|
459
730
|
const declared = config?.law ?? {};
|
|
460
731
|
const file = typeof declared.file === "string" ? declared.file : LAW;
|
|
461
|
-
const ceiling = typeof declared.maxLines === "number" ? declared.maxLines : CEILING;
|
|
462
732
|
const path = join4(root, file);
|
|
463
733
|
if (!existsSync3(path)) {
|
|
464
734
|
return [finding3(file, "SKIP", "there is no law file here to measure")];
|
|
465
735
|
}
|
|
466
736
|
const source = readFileSync4(path, "utf8");
|
|
467
737
|
const lines = source.split("\n").length - (source.endsWith("\n") ? 1 : 0);
|
|
738
|
+
if (typeof declared.maxLines !== "number") {
|
|
739
|
+
return [
|
|
740
|
+
finding3(
|
|
741
|
+
file,
|
|
742
|
+
"SKIP",
|
|
743
|
+
`${lines} lines, and no ceiling to measure them against \u2014 set law.maxLines in geonosis.json to have this asked (${CEILING} is what both source repos converged on)`
|
|
744
|
+
)
|
|
745
|
+
];
|
|
746
|
+
}
|
|
747
|
+
const ceiling = declared.maxLines;
|
|
468
748
|
return [
|
|
469
749
|
lines > ceiling ? finding3(
|
|
470
750
|
file,
|
|
@@ -473,20 +753,40 @@ var law = (root, config) => {
|
|
|
473
753
|
) : finding3(file, "OK", `${lines} lines, under the ceiling of ${ceiling}`)
|
|
474
754
|
];
|
|
475
755
|
};
|
|
476
|
-
var
|
|
756
|
+
var KIT_PLUGIN = "geonosis";
|
|
757
|
+
var enablesKit = (path) => {
|
|
758
|
+
if (!existsSync3(path)) return false;
|
|
759
|
+
try {
|
|
760
|
+
const parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
761
|
+
const enabled = parsed.enabledPlugins;
|
|
762
|
+
if (typeof enabled !== "object" || enabled === null) return false;
|
|
763
|
+
return Object.entries(enabled).some(
|
|
764
|
+
([id, on]) => on !== false && id.split("@")[0] === KIT_PLUGIN
|
|
765
|
+
);
|
|
766
|
+
} catch {
|
|
767
|
+
return false;
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
var hooks = (root, userSettings) => {
|
|
477
771
|
const path = join4(root, SETTINGS);
|
|
478
|
-
if (
|
|
772
|
+
if (enablesKit(path)) {
|
|
773
|
+
return [finding3(SETTINGS, "OK", `\`enabledPlugins\` here enables the kit\u2019s plugin`)];
|
|
774
|
+
}
|
|
775
|
+
if (enablesKit(userSettings)) {
|
|
479
776
|
return [
|
|
480
777
|
finding3(
|
|
481
778
|
SETTINGS,
|
|
482
|
-
"
|
|
483
|
-
|
|
779
|
+
"OK",
|
|
780
|
+
`installed elsewhere \u2014 not this repo\u2019s to declare: \`enabledPlugins\` in ${userSettings} enables it for every repo on this machine`
|
|
484
781
|
)
|
|
485
782
|
];
|
|
486
783
|
}
|
|
487
|
-
const source = readFileSync4(path, "utf8");
|
|
488
784
|
return [
|
|
489
|
-
|
|
785
|
+
finding3(
|
|
786
|
+
SETTINGS,
|
|
787
|
+
"WARN",
|
|
788
|
+
`nothing enables the kit\u2019s plugin, so none of the hooks, agents or skills reach this repo \u2014 the gates run, the method does not. What satisfies this: a "geonosis@<marketplace>" entry under \`enabledPlugins\` in ${SETTINGS} here, or the same in the user-scope ${userSettings}`
|
|
789
|
+
)
|
|
490
790
|
];
|
|
491
791
|
};
|
|
492
792
|
var READERS = {
|
|
@@ -504,7 +804,17 @@ var resolves = (root, name) => {
|
|
|
504
804
|
return false;
|
|
505
805
|
}
|
|
506
806
|
};
|
|
507
|
-
var
|
|
807
|
+
var declaredAnywhere = (workspaces) => new Set(
|
|
808
|
+
workspaces.flatMap(
|
|
809
|
+
(one) => [
|
|
810
|
+
one.manifest.dependencies,
|
|
811
|
+
one.manifest.devDependencies,
|
|
812
|
+
one.manifest.optionalDependencies,
|
|
813
|
+
one.manifest.peerDependencies
|
|
814
|
+
].flatMap((field) => Object.keys(field ?? {}))
|
|
815
|
+
)
|
|
816
|
+
);
|
|
817
|
+
var blocks = (root, config, readers, workspaces) => {
|
|
508
818
|
if (config === void 0) {
|
|
509
819
|
return [
|
|
510
820
|
finding3(
|
|
@@ -514,19 +824,30 @@ var blocks = (root, config, readers) => {
|
|
|
514
824
|
)
|
|
515
825
|
];
|
|
516
826
|
}
|
|
827
|
+
const declaredPackages = declaredAnywhere(workspaces);
|
|
517
828
|
return Object.entries(readers).flatMap(([block, name]) => {
|
|
518
829
|
const declared = config[block] !== void 0;
|
|
830
|
+
const chosen = declaredPackages.has(name);
|
|
519
831
|
const installed = resolves(root, name);
|
|
832
|
+
if (declared && !chosen) {
|
|
833
|
+
return [
|
|
834
|
+
finding3(
|
|
835
|
+
name,
|
|
836
|
+
"WARN",
|
|
837
|
+
`geonosis.json has a "${block}" block and no manifest here declares ${name} \u2014 nothing reads it${installed ? ", and the copy still under node_modules is what a stale install left behind" : ""}`
|
|
838
|
+
)
|
|
839
|
+
];
|
|
840
|
+
}
|
|
520
841
|
if (declared && !installed) {
|
|
521
842
|
return [
|
|
522
843
|
finding3(
|
|
523
844
|
name,
|
|
524
845
|
"WARN",
|
|
525
|
-
`geonosis.json has a "${block}" block
|
|
846
|
+
`geonosis.json has a "${block}" block, a manifest declares ${name}, and it does not resolve here \u2014 run the install`
|
|
526
847
|
)
|
|
527
848
|
];
|
|
528
849
|
}
|
|
529
|
-
if (!declared && installed) {
|
|
850
|
+
if (!declared && chosen && installed) {
|
|
530
851
|
return [
|
|
531
852
|
finding3(
|
|
532
853
|
name,
|
|
@@ -535,29 +856,54 @@ var blocks = (root, config, readers) => {
|
|
|
535
856
|
)
|
|
536
857
|
];
|
|
537
858
|
}
|
|
859
|
+
if (!declared && installed) {
|
|
860
|
+
return [
|
|
861
|
+
finding3(
|
|
862
|
+
name,
|
|
863
|
+
"OK",
|
|
864
|
+
`${name} is here but no manifest declares it \u2014 a transitive dependency this repo did not choose, so it wants no "${block}" block`
|
|
865
|
+
)
|
|
866
|
+
];
|
|
867
|
+
}
|
|
538
868
|
return declared ? [finding3(name, "OK", `a "${block}" block, and ${name} to read it`)] : [];
|
|
539
869
|
});
|
|
540
870
|
};
|
|
541
|
-
var
|
|
871
|
+
var PLUGIN_DIR_RULE = "biological-architecture/no-unregistered-plugin-dir";
|
|
872
|
+
var registryList = (value) => {
|
|
873
|
+
if (typeof value === "string") return [value];
|
|
874
|
+
if (Array.isArray(value) && value.every((one) => typeof one === "string")) return value;
|
|
875
|
+
return void 0;
|
|
876
|
+
};
|
|
877
|
+
var pluginDirsOf = (level) => {
|
|
878
|
+
const options = Array.isArray(level) ? level[1] : void 0;
|
|
879
|
+
const registry = registryList(options?.registry);
|
|
880
|
+
if (options?.roots === void 0 || registry === void 0) return void 0;
|
|
881
|
+
return { manifests: options.manifests ?? ["index.ts"], registry, roots: options.roots };
|
|
882
|
+
};
|
|
883
|
+
var pluginDirLayers = (root) => {
|
|
542
884
|
const path = join4(root, ".oxlintrc.json");
|
|
543
|
-
if (!existsSync3(path)) return
|
|
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;
|
|
885
|
+
if (!existsSync3(path)) return [];
|
|
886
|
+
const layers = [];
|
|
887
|
+
for (const layer of layersOf(readConfig(path, root), PLUGIN_DIR_RULE)) {
|
|
888
|
+
const dirs = pluginDirsOf(layer.level);
|
|
889
|
+
if (dirs !== void 0) layers.push({ dirs, files: layer.files });
|
|
556
890
|
}
|
|
891
|
+
return layers;
|
|
892
|
+
};
|
|
893
|
+
var layerOver = (layers, root, relative) => {
|
|
894
|
+
const at = join4(root, relative);
|
|
895
|
+
const inside = readdirSync2(at, { withFileTypes: true }).filter((entry) => entry.isFile()).map((entry) => `${relative}/${entry.name}`);
|
|
896
|
+
const paths = inside.length === 0 ? [relative] : inside;
|
|
897
|
+
const wrapped = layers.map((layer) => ({ files: layer.files, level: layer.dirs }));
|
|
898
|
+
for (const path of paths) {
|
|
899
|
+
const found = governing(wrapped, path);
|
|
900
|
+
if (found !== void 0) return found.level;
|
|
901
|
+
}
|
|
902
|
+
return void 0;
|
|
557
903
|
};
|
|
558
904
|
var pluginDirs = (root) => {
|
|
559
|
-
const
|
|
560
|
-
if (
|
|
905
|
+
const layers = pluginDirLayers(root);
|
|
906
|
+
if (layers.length === 0) {
|
|
561
907
|
return [
|
|
562
908
|
finding3(
|
|
563
909
|
"plugin directories",
|
|
@@ -566,50 +912,180 @@ var pluginDirs = (root) => {
|
|
|
566
912
|
)
|
|
567
913
|
];
|
|
568
914
|
}
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
|
|
915
|
+
const missing = [...new Set(layers.flatMap((layer) => layer.dirs.registry))].filter(
|
|
916
|
+
(registry) => !existsSync3(join4(root, registry))
|
|
917
|
+
);
|
|
918
|
+
if (missing.length > 0) {
|
|
919
|
+
return missing.map(
|
|
920
|
+
(registry) => finding3(registry, "FAIL", "the registry the rule names is not there")
|
|
921
|
+
);
|
|
572
922
|
}
|
|
573
|
-
const
|
|
574
|
-
const unreachable =
|
|
575
|
-
for (const rootDir of
|
|
923
|
+
const source = /* @__PURE__ */ new Map();
|
|
924
|
+
const unreachable = /* @__PURE__ */ new Map();
|
|
925
|
+
for (const rootDir of new Set(layers.flatMap((layer) => layer.dirs.roots))) {
|
|
576
926
|
const at = join4(root, rootDir);
|
|
577
927
|
if (!existsSync3(at)) continue;
|
|
578
928
|
for (const entry of readdirSync2(at, { withFileTypes: true })) {
|
|
579
929
|
if (!entry.isDirectory()) continue;
|
|
580
|
-
const
|
|
581
|
-
|
|
930
|
+
const relative = `${rootDir}/${entry.name}`;
|
|
931
|
+
const governs = layerOver(layers, root, relative);
|
|
932
|
+
if (governs === void 0) continue;
|
|
933
|
+
const key = governs.registry.join(", ");
|
|
934
|
+
const registry = source.get(key) ?? governs.registry.map((half) => readFileSync4(join4(root, half), "utf8")).join("\n");
|
|
935
|
+
source.set(key, registry);
|
|
936
|
+
const hasManifest = governs.manifests.some((name) => existsSync3(join4(at, entry.name, name)));
|
|
937
|
+
if (!hasManifest || registry.includes(entry.name)) continue;
|
|
938
|
+
unreachable.set(key, [...unreachable.get(key) ?? [], relative]);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
return [...source.keys()].toSorted().map((registry) => {
|
|
942
|
+
const named2 = unreachable.get(registry) ?? [];
|
|
943
|
+
return named2.length === 0 ? finding3(registry, "OK", "every directory under the declared roots is named by it") : finding3(
|
|
944
|
+
registry,
|
|
945
|
+
"FAIL",
|
|
946
|
+
`${named2.length} directory(ies) it never names: ${named2.join(", ")}`
|
|
947
|
+
);
|
|
948
|
+
});
|
|
949
|
+
};
|
|
950
|
+
var WORKSPACE_YAML = "pnpm-workspace.yaml";
|
|
951
|
+
var HOIST_KEY = "publicHoistPattern";
|
|
952
|
+
var HOIST_REPAIR = "rm -rf node_modules/.modules.yaml node_modules/.pnpm-workspace-state-v1.json && pnpm install";
|
|
953
|
+
var hoistPatterns = (root) => {
|
|
954
|
+
const path = join4(root, WORKSPACE_YAML);
|
|
955
|
+
if (!existsSync3(path)) return void 0;
|
|
956
|
+
const lines = readFileSync4(path, "utf8").split("\n");
|
|
957
|
+
const at = lines.findIndex((line) => new RegExp(`^${HOIST_KEY}\\s*:`).test(line));
|
|
958
|
+
if (at < 0) return void 0;
|
|
959
|
+
const patterns = [];
|
|
960
|
+
for (const line of lines.slice(at + 1)) {
|
|
961
|
+
if (/^\s*(?:#.*)?$/.test(line)) continue;
|
|
962
|
+
const item = /^\s+-\s*(.+?)\s*$/.exec(line);
|
|
963
|
+
if (item?.[1] === void 0) break;
|
|
964
|
+
patterns.push(item[1].replace(/^['"]|['"]$/g, ""));
|
|
965
|
+
}
|
|
966
|
+
return patterns;
|
|
967
|
+
};
|
|
968
|
+
var matching = (pattern) => new RegExp(`^${pattern.replaceAll(/[.+?^${}()|[\]\\]/g, "\\$&").replaceAll("*", ".*")}$`);
|
|
969
|
+
var publicHoists = (root, workspaces) => {
|
|
970
|
+
const patterns = hoistPatterns(root);
|
|
971
|
+
if (patterns === void 0) {
|
|
972
|
+
return [
|
|
973
|
+
finding3(
|
|
974
|
+
WORKSPACE_YAML,
|
|
975
|
+
"SKIP",
|
|
976
|
+
`no ${HOIST_KEY} is declared here, so nothing claims a package should be linked at the root`
|
|
977
|
+
)
|
|
978
|
+
];
|
|
979
|
+
}
|
|
980
|
+
const named2 = workspaces.flatMap(
|
|
981
|
+
(one) => one.dir === root || one.manifest.name === void 0 ? [] : [one.manifest.name]
|
|
982
|
+
);
|
|
983
|
+
return patterns.map((pattern) => {
|
|
984
|
+
const shape = matching(pattern);
|
|
985
|
+
const hoisted = named2.filter((name) => shape.test(name));
|
|
986
|
+
if (hoisted.length === 0) {
|
|
987
|
+
return finding3(
|
|
988
|
+
pattern,
|
|
989
|
+
"SKIP",
|
|
990
|
+
`this ${HOIST_KEY} matches no workspace package here, so what it should have linked at the root is a question this cannot answer`
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
const pruned = hoisted.filter((name) => !existsSync3(join4(root, "node_modules", name)));
|
|
994
|
+
if (pruned.length === 0) {
|
|
995
|
+
return finding3(
|
|
996
|
+
pattern,
|
|
997
|
+
"OK",
|
|
998
|
+
`every workspace package this ${HOIST_KEY} matches is linked at the root`
|
|
582
999
|
);
|
|
583
|
-
|
|
584
|
-
|
|
1000
|
+
}
|
|
1001
|
+
return finding3(
|
|
1002
|
+
pattern,
|
|
1003
|
+
"FAIL",
|
|
1004
|
+
`the ${HOIST_KEY} \`${pattern}\` has no root link for ${pruned.map((name) => `node_modules/${name}`).join(", ")} \u2014 pnpm prunes one during an unrelated add and then answers "Already up to date" over a clean git status forever. Repair: ${HOIST_REPAIR}`
|
|
1005
|
+
);
|
|
1006
|
+
});
|
|
1007
|
+
};
|
|
1008
|
+
var HOOK_FILES = ["lefthook.yml", "lefthook.yaml", ".lefthook.yml", ".lefthook.yaml"];
|
|
1009
|
+
var HOOK_DIRS = [".husky", ".githooks"];
|
|
1010
|
+
var VERSION_MANAGED = /(?:^|[\s;&|(])(?<runner>pnpm|npx|bunx|yarn)\b(?:\s+(?:exec|run|dlx|x))?\s+(?<bin>geonosis(?:-[a-z-]+)?)\b/;
|
|
1011
|
+
var NAMES_A_BIN = /(?:^|[\s;&|(/])geonosis(?:-[a-z-]+)?\b/;
|
|
1012
|
+
var hookLines = (root) => {
|
|
1013
|
+
const found = [];
|
|
1014
|
+
const read = (path) => {
|
|
1015
|
+
const at = relativePath(root, path);
|
|
1016
|
+
for (const line of readFileSync4(path, "utf8").split("\n")) {
|
|
1017
|
+
if (line.trim() !== "") found.push({ at, line });
|
|
1018
|
+
}
|
|
1019
|
+
};
|
|
1020
|
+
for (const name of HOOK_FILES) {
|
|
1021
|
+
const path = join4(root, name);
|
|
1022
|
+
if (existsSync3(path)) read(path);
|
|
1023
|
+
}
|
|
1024
|
+
for (const name of HOOK_DIRS) {
|
|
1025
|
+
const dir = join4(root, name);
|
|
1026
|
+
if (!existsSync3(dir)) continue;
|
|
1027
|
+
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
1028
|
+
if (entry.isFile() && !entry.name.startsWith("_") && !entry.name.startsWith(".")) {
|
|
1029
|
+
read(join4(dir, entry.name));
|
|
585
1030
|
}
|
|
586
1031
|
}
|
|
587
1032
|
}
|
|
588
|
-
return
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
)
|
|
598
|
-
|
|
1033
|
+
return found;
|
|
1034
|
+
};
|
|
1035
|
+
var gitHooks = (root) => {
|
|
1036
|
+
const lines = hookLines(root);
|
|
1037
|
+
if (lines.length === 0) {
|
|
1038
|
+
return [finding3("git hooks", "SKIP", "no lefthook, husky or .githooks file here to read")];
|
|
1039
|
+
}
|
|
1040
|
+
const files = [...new Set(lines.map((one) => one.at))].toSorted();
|
|
1041
|
+
return files.flatMap((at) => {
|
|
1042
|
+
const here = lines.filter((one) => one.at === at);
|
|
1043
|
+
const named2 = here.filter((one) => NAMES_A_BIN.test(one.line));
|
|
1044
|
+
if (named2.length === 0) {
|
|
1045
|
+
return [finding3(at, "SKIP", "it names no geonosis bin, so there is nothing here to start")];
|
|
1046
|
+
}
|
|
1047
|
+
const wrong = named2.flatMap((one) => {
|
|
1048
|
+
const found = VERSION_MANAGED.exec(one.line)?.groups;
|
|
1049
|
+
return found === void 0 ? [] : [{ bin: found["bin"] ?? "", runner: found["runner"] ?? "" }];
|
|
1050
|
+
});
|
|
1051
|
+
if (wrong.length === 0) {
|
|
1052
|
+
return [
|
|
1053
|
+
finding3(
|
|
1054
|
+
at,
|
|
1055
|
+
"OK",
|
|
1056
|
+
`${named2.length} geonosis bin(s) here, every one called directly rather than through a runner`
|
|
1057
|
+
)
|
|
1058
|
+
];
|
|
1059
|
+
}
|
|
1060
|
+
return wrong.map(
|
|
1061
|
+
({ bin, runner }) => finding3(
|
|
1062
|
+
at,
|
|
1063
|
+
"FAIL",
|
|
1064
|
+
`it runs ${bin} through ${runner} \u2014 a git hook's PATH is not the shell's and carries no version-manager shim, so ${runner} fails to START and what the author reads is its message, not this gate's. Call it directly: node_modules/.bin/${bin}`
|
|
1065
|
+
)
|
|
1066
|
+
);
|
|
1067
|
+
});
|
|
599
1068
|
};
|
|
600
1069
|
var checkDrift = ({
|
|
601
1070
|
readers = READERS,
|
|
602
1071
|
root,
|
|
1072
|
+
userSettings = join4(homedir(), SETTINGS),
|
|
603
1073
|
workspaces
|
|
604
1074
|
}) => {
|
|
605
1075
|
const config = readGeonosis(root);
|
|
606
1076
|
return [
|
|
607
1077
|
...ci(root),
|
|
608
1078
|
...orphanTests(root, workspaces),
|
|
1079
|
+
...scriptPaths(workspaces),
|
|
1080
|
+
...linkedBins(root, workspaces),
|
|
1081
|
+
...preloadAroundPnpm(root, workspaces),
|
|
1082
|
+
...generatedFiles(root, workspaces),
|
|
609
1083
|
...pluginDirs(root),
|
|
1084
|
+
...publicHoists(root, workspaces),
|
|
1085
|
+
...gitHooks(root),
|
|
610
1086
|
...law(root, config),
|
|
611
|
-
...hooks(root),
|
|
612
|
-
...blocks(root, config, readers)
|
|
1087
|
+
...hooks(root, userSettings),
|
|
1088
|
+
...blocks(root, config, readers, workspaces)
|
|
613
1089
|
];
|
|
614
1090
|
};
|
|
615
1091
|
|
|
@@ -672,7 +1148,38 @@ var ownReach = ({
|
|
|
672
1148
|
};
|
|
673
1149
|
};
|
|
674
1150
|
var optionsOf = (level) => Array.isArray(level) ? level.slice(1) : [];
|
|
675
|
-
var
|
|
1151
|
+
var optionSetsOf = (config, rule) => {
|
|
1152
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1153
|
+
const sets = [];
|
|
1154
|
+
for (const layer of layersOf(config, rule)) {
|
|
1155
|
+
const options = [...optionsOf(layer.level)];
|
|
1156
|
+
const key = JSON.stringify(options);
|
|
1157
|
+
if (seen.has(key)) continue;
|
|
1158
|
+
seen.add(key);
|
|
1159
|
+
sets.push(options);
|
|
1160
|
+
}
|
|
1161
|
+
return sets;
|
|
1162
|
+
};
|
|
1163
|
+
var enablingLayersOf = (config, rule) => {
|
|
1164
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1165
|
+
const layers = [];
|
|
1166
|
+
for (const layer of layersOf(config, rule)) {
|
|
1167
|
+
if (OFF.has(severityOf(layer.level))) continue;
|
|
1168
|
+
const options = [...optionsOf(layer.level)];
|
|
1169
|
+
const key = JSON.stringify([layer.files, options]);
|
|
1170
|
+
if (seen.has(key)) continue;
|
|
1171
|
+
seen.add(key);
|
|
1172
|
+
layers.push({ files: layer.files, options });
|
|
1173
|
+
}
|
|
1174
|
+
return layers;
|
|
1175
|
+
};
|
|
1176
|
+
var under = (config, rule) => `${rule} under ${optionSetsOf(config, rule).map((options) => JSON.stringify(options)).join(" / ")}`;
|
|
1177
|
+
var enabledHere = (config, plugin) => [
|
|
1178
|
+
.../* @__PURE__ */ new Set([
|
|
1179
|
+
...enabledRulesOf(config.rules, plugin),
|
|
1180
|
+
...config.overrides.flatMap((one) => enabledRulesOf(one.rules, plugin))
|
|
1181
|
+
])
|
|
1182
|
+
].toSorted();
|
|
676
1183
|
var throughProbes = ({
|
|
677
1184
|
config,
|
|
678
1185
|
corpus,
|
|
@@ -681,6 +1188,7 @@ var throughProbes = ({
|
|
|
681
1188
|
silent
|
|
682
1189
|
}) => {
|
|
683
1190
|
const refused = /* @__PURE__ */ new Map();
|
|
1191
|
+
const placed = /* @__PURE__ */ new Set();
|
|
684
1192
|
const dir = mkdtempSync(join5(tmpdir(), "geonosis-doctor-probe-"));
|
|
685
1193
|
const here = join5(dir, "corpus");
|
|
686
1194
|
try {
|
|
@@ -689,10 +1197,29 @@ var throughProbes = ({
|
|
|
689
1197
|
const write = probes[rule];
|
|
690
1198
|
if (write === void 0) continue;
|
|
691
1199
|
try {
|
|
692
|
-
const
|
|
693
|
-
const
|
|
694
|
-
|
|
695
|
-
|
|
1200
|
+
const layers = enablingLayersOf(config, rule);
|
|
1201
|
+
const unclaimed = [];
|
|
1202
|
+
let anyPlaced = false;
|
|
1203
|
+
for (const layer of layers) {
|
|
1204
|
+
const files = write(layer.options);
|
|
1205
|
+
if (files.length === 0) throw new Error("its probe declares no file at all");
|
|
1206
|
+
const claims = layer.files.length === 0 || files.some((file) => layer.files.some((glob) => matchesGlob(glob, file.path)));
|
|
1207
|
+
if (!claims) {
|
|
1208
|
+
unclaimed.push(
|
|
1209
|
+
`its probe lands at ${files[0]?.path ?? ""}, which the entry enabling it claims none of: ${layer.files.join(", ")}`
|
|
1210
|
+
);
|
|
1211
|
+
continue;
|
|
1212
|
+
}
|
|
1213
|
+
for (const file of files) {
|
|
1214
|
+
const at = join5(here, file.path);
|
|
1215
|
+
if (placed.has(at)) continue;
|
|
1216
|
+
placed.add(at);
|
|
1217
|
+
mkdirSync(dirname2(at), { recursive: true });
|
|
1218
|
+
writeFileSync(at, file.source);
|
|
1219
|
+
}
|
|
1220
|
+
anyPlaced = true;
|
|
1221
|
+
}
|
|
1222
|
+
if (!anyPlaced) throw new Error(unclaimed.join(" \xB7 "));
|
|
696
1223
|
} catch (error) {
|
|
697
1224
|
refused.set(rule, String(error.message));
|
|
698
1225
|
}
|
|
@@ -735,7 +1262,7 @@ var checkExercised = async ({
|
|
|
735
1262
|
} catch (error) {
|
|
736
1263
|
return said("SKIP", refusal(error));
|
|
737
1264
|
}
|
|
738
|
-
const enabled =
|
|
1265
|
+
const enabled = enabledHere(config, manifest.plugin);
|
|
739
1266
|
if (enabled.length === 0) return said("SKIP", `no ${manifest.plugin} rule is enabled here`);
|
|
740
1267
|
let reach;
|
|
741
1268
|
try {
|
|
@@ -796,7 +1323,7 @@ var checkExercised = async ({
|
|
|
796
1323
|
}
|
|
797
1324
|
const unplaceable = [
|
|
798
1325
|
...unprobed.map(
|
|
799
|
-
(rule) => `${rule} declares no probe, so ${
|
|
1326
|
+
(rule) => `${rule} declares no probe, so ${optionSetsOf(config, rule).every((options) => options.length === 0) ? "its scope" : under(config, rule)} does not reach the corpus`
|
|
800
1327
|
),
|
|
801
1328
|
...inert.map((rule) => `${rule}: ${probed.refused.get(rule) ?? ""}`)
|
|
802
1329
|
];
|
|
@@ -1199,6 +1726,12 @@ var checkRunner = ({
|
|
|
1199
1726
|
`"${script}" runs neither vitest nor bun test \u2014 this check has nothing to say about it`
|
|
1200
1727
|
);
|
|
1201
1728
|
}
|
|
1729
|
+
if (testFilesUnder(workspace.dir).length === 0) {
|
|
1730
|
+
return said(
|
|
1731
|
+
"SKIP",
|
|
1732
|
+
`"${script}" runs a test runner and there is no test file under this workspace \u2014 nothing here for its exit code to be wrong about`
|
|
1733
|
+
);
|
|
1734
|
+
}
|
|
1202
1735
|
if (WRITES_A_REPORT.test(script)) {
|
|
1203
1736
|
return said("OK", "the script asks the runner for its own JSON report, not for a status code");
|
|
1204
1737
|
}
|
|
@@ -1283,6 +1816,11 @@ var runDoctor = async ({
|
|
|
1283
1816
|
}) => {
|
|
1284
1817
|
const configs = discoverConfigs(root);
|
|
1285
1818
|
const workspaces = discoverWorkspaces(root);
|
|
1819
|
+
if (workspaces.length === 0 && configs.length === 0) {
|
|
1820
|
+
throw new DoctorError(
|
|
1821
|
+
`nothing here to examine \u2014 no package.json and no ${CONFIG_FILE} anywhere under ${root}. Every line this would print is about a gate that is not here, and a page of SKIPs is not a pass.`
|
|
1822
|
+
);
|
|
1823
|
+
}
|
|
1286
1824
|
const asked = (check) => only === void 0 || only.includes(check);
|
|
1287
1825
|
const runs = [
|
|
1288
1826
|
["loaded", () => checkLoaded({ configs, root, workspaces })],
|
package/dist/doctor-cli.js
CHANGED
|
@@ -3,58 +3,65 @@ import {
|
|
|
3
3
|
formatDoctor,
|
|
4
4
|
formatJson,
|
|
5
5
|
runDoctor
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-7BRGHYQR.js";
|
|
7
7
|
|
|
8
8
|
// src/doctor-cli.ts
|
|
9
9
|
import { resolve } from "path";
|
|
10
|
-
var
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
var ABOUT = {
|
|
11
|
+
baseline: `The ratchet's numbers at HEAD against another ref. A ratchet lowers its own baseline
|
|
12
|
+
when a number shrinks, so nothing inside one checkout can see a branch raise one back
|
|
13
|
+
up. OFF unless asked for: it is the only check that runs git, and this tool is run
|
|
14
|
+
over repos other people are working in.`,
|
|
15
|
+
deployed: `What the pipeline REPORTED deploying, in .geonosis/deployed.json, against what the
|
|
16
|
+
tree declares in the wrangler configs geonosis.json names and release.secrets.
|
|
17
|
+
"versions upload" applies no triggers, so a cron edited in a config is silently
|
|
18
|
+
ignored in production for ever with a green pipeline; a secret can be put outside the
|
|
19
|
+
gate entirely. SKIPs with the sentence when the file is absent \u2014 its absence is not a
|
|
20
|
+
pass \u2014 and reads the file rather than importing @geonosis/release.`,
|
|
21
|
+
drift: `The gates that were set up and are no longer running: a CI job switched off by a
|
|
22
|
+
condition that can never be true, a test file under a workspace with no test script, a
|
|
23
|
+
script handing bun/node/tsx a file that is not on disk, a workspace bin nothing links,
|
|
24
|
+
an integration directory the registry never names, a geonosis bin a git hook calls
|
|
25
|
+
through pnpm/npx/bunx and therefore cannot start, a law over the ceiling its own
|
|
26
|
+
geonosis.json declared, nothing enabling the plugin in either scope, and a
|
|
27
|
+
geonosis.json block no manifest declares a package for (or the reverse).`,
|
|
28
|
+
exercised: `Every rule a config enables, against the corpus the loaded plugin ships. A rule at
|
|
29
|
+
"error" that can never fire is indistinguishable from a clean tree.`,
|
|
30
|
+
loaded: `The plugin oxlint would LOAD from each config's directory, against the version that
|
|
16
31
|
config's workspace DECLARES. oxlint resolves a jsPlugins specifier from the CONFIG
|
|
17
32
|
FILE's directory, so a nested copy left behind by a per-workspace install runs while
|
|
18
33
|
every manifest and the lockfile say otherwise \u2014 one consumer measured a whole bug
|
|
19
34
|
report against 0.3.0 on a repo pinned to 0.4.0. Every copy is found by RESOLUTION,
|
|
20
|
-
never by find: on pnpm the store keeps every version ever installed
|
|
21
|
-
|
|
22
|
-
exercised Every rule a config enables, against the corpus the loaded plugin ships. A rule at
|
|
23
|
-
"error" that can never fire is indistinguishable from a clean tree.
|
|
24
|
-
|
|
25
|
-
baseline The ratchet's numbers at HEAD against another ref. A ratchet lowers its own baseline
|
|
26
|
-
when a number shrinks, so nothing inside one checkout can see a branch raise one back
|
|
27
|
-
up. OFF unless asked for: it is the only check that runs git, and this tool is run
|
|
28
|
-
over repos other people are working in.
|
|
29
|
-
|
|
30
|
-
runner Workspaces whose test script's exit code is the only verdict. A pool exited 0 over
|
|
31
|
-
suites it had just reported as failing, for weeks, in a consumer.
|
|
32
|
-
|
|
33
|
-
observability
|
|
34
|
-
The exporter named by the "observability" block in geonosis.json: is a sink
|
|
35
|
+
never by find: on pnpm the store keeps every version ever installed.`,
|
|
36
|
+
observability: `The exporter named by the "observability" block in geonosis.json: is a sink
|
|
35
37
|
configured, is its endpoint reachable (a HEAD with a short timeout), and did an event
|
|
36
38
|
arrive inside maxAgeSeconds \u2014 read from the lastEventFile the sink writes, never from
|
|
37
39
|
this tool importing the library it is checking. A deploy job reported success having
|
|
38
40
|
deployed nothing and a /health answered ok over a dead database on the same
|
|
39
|
-
afternoon; both left a green build and a silent project
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
afternoon; both left a green build and a silent project.`,
|
|
42
|
+
runner: `Workspaces whose test script's exit code is the only verdict. A pool exited 0 over
|
|
43
|
+
suites it had just reported as failing, for weeks, in a consumer. A script with no
|
|
44
|
+
test file under it is a SKIP \u2014 there is no result for an exit code to be wrong about.`
|
|
45
|
+
};
|
|
46
|
+
var USAGE = `geonosis-doctor [--root <dir>] [--only <check,\u2026>] [--json] [--strict] [--baseline-against [<ref>]] [--oxlint <path>]
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
${CHECKS.length} questions a version bump is not finished until something has asked. Most are ways
|
|
49
|
+
enforcement has reported green while measuring nothing; drift asks whether the gate is still there
|
|
50
|
+
at all, and the last two ask whether what was reported is what happened.
|
|
51
|
+
|
|
52
|
+
With no arguments it runs every one of them over the working directory. A tree holding no
|
|
53
|
+
package.json and no .oxlintrc.json is REFUSED with exit 2 \u2014 there is nothing there to examine, and a
|
|
54
|
+
page of SKIPs is not a pass.
|
|
55
|
+
|
|
56
|
+
${CHECKS.map((check) => ` ${check.padEnd(11)} ${ABOUT[check]}`).join("\n\n")}
|
|
52
57
|
|
|
53
58
|
exercised reads the corpus the loaded plugin ships, and \u2014 when geonosis.json names one under
|
|
54
59
|
doctor.corpus \u2014 the corpus THIS repo ships for its own options. Three rules cannot be answered any
|
|
55
60
|
other way: layer-walls fires on a repo's own layer names, no-brand-names on its brand, and
|
|
56
61
|
plugin-route-namespaced on its package root, while the shipped corpus says acme and layers/core.
|
|
57
62
|
|
|
63
|
+
--print-config-shape every file this reads, and the keys it reads out of each
|
|
64
|
+
|
|
58
65
|
Exits 1 when any line is a FAIL or an UNJUDGED \u2014 a question this could not ask is not a pass \u2014 and
|
|
59
66
|
2 when the run could not be made at all. --strict promotes every WARN to a FAIL. --json prints the
|
|
60
67
|
whole report for a CI step to read.`;
|
|
@@ -100,10 +107,43 @@ var parseDoctorArgs = (argv, cwd) => {
|
|
|
100
107
|
strict
|
|
101
108
|
};
|
|
102
109
|
};
|
|
110
|
+
var CONFIG_SHAPE = `geonosis-doctor reads geonosis.json, geonosis.ratchet.json, .oxlintrc.json,
|
|
111
|
+
pnpm-workspace.yaml and .claude/settings.json
|
|
112
|
+
|
|
113
|
+
geonosis.json \u2192 "law"
|
|
114
|
+
file string? the law file to measure (default "CLAUDE.md")
|
|
115
|
+
maxLines number? the ceiling. Undeclared means UNJUDGED, never a default it fires on
|
|
116
|
+
geonosis.json \u2192 "observability"
|
|
117
|
+
sink string where this repo sends its errors
|
|
118
|
+
endpoint string the URL that must be reachable
|
|
119
|
+
lastEventFile string the file the sink writes on arrival
|
|
120
|
+
probe string? a command that answers instead of lastEventFile
|
|
121
|
+
maxAgeSeconds number how old the last event may be
|
|
122
|
+
geonosis.json \u2192 "release"
|
|
123
|
+
wrangler string[] the wrangler configs a deployment carries
|
|
124
|
+
wranglerEnv string? the named environment block to read
|
|
125
|
+
secrets string[] the secret names it carries
|
|
126
|
+
.oxlintrc.json
|
|
127
|
+
jsPlugins string[] the plugin specifiers oxlint would load
|
|
128
|
+
rules object the base rules, and the options each was given
|
|
129
|
+
overrides object[] files + rules; an entry REPLACES a rule's options for the files it
|
|
130
|
+
claims, last match winning \u2014 the same resolution oxlint does
|
|
131
|
+
geonosis.ratchet.json
|
|
132
|
+
the ratchet's own \u2014 run \`geonosis-ratchet --print-config-shape\`
|
|
133
|
+
pnpm-workspace.yaml
|
|
134
|
+
publicHoistPattern string[] the packages this repo claims are linked at the root, read as
|
|
135
|
+
text; a "*" is the only wildcard and it spans "/"
|
|
136
|
+
.claude/settings.json
|
|
137
|
+
enabledPlugins object what installs the kit's plugin in THIS repo`;
|
|
103
138
|
var main = async () => {
|
|
104
139
|
const argv = process.argv.slice(2);
|
|
105
140
|
if (argv.includes("--help") || argv.includes("-h")) {
|
|
106
141
|
process.stdout.write(`${USAGE}
|
|
142
|
+
`);
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
if (argv.includes("--print-config-shape")) {
|
|
146
|
+
process.stdout.write(`${CONFIG_SHAPE}
|
|
107
147
|
`);
|
|
108
148
|
return 0;
|
|
109
149
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
type Overrides = {
|
|
2
|
+
files: readonly string[];
|
|
3
|
+
rules: Record<string, unknown>;
|
|
4
|
+
}[];
|
|
5
|
+
|
|
1
6
|
/**
|
|
2
7
|
* The questions a bump is not finished until something has asked, in the order a run asks them.
|
|
3
8
|
* The first four are ways enforcement has reported green while measuring nothing; `drift` asks
|
|
@@ -29,6 +34,7 @@ declare class DoctorError extends Error {
|
|
|
29
34
|
constructor(message: string);
|
|
30
35
|
}
|
|
31
36
|
type Manifest = {
|
|
37
|
+
bin?: Record<string, string> | string;
|
|
32
38
|
dependencies?: Record<string, string>;
|
|
33
39
|
devDependencies?: Record<string, string>;
|
|
34
40
|
name?: string;
|
|
@@ -47,6 +53,8 @@ type DiscoveredConfig = {
|
|
|
47
53
|
/** Why this config could not be read. A config nobody can parse is a finding, never a skip. */
|
|
48
54
|
error?: string;
|
|
49
55
|
jsPlugins: string[];
|
|
56
|
+
/** Layers that REPLACE a rule's options for the files they claim — never merged into the base. */
|
|
57
|
+
overrides: Overrides;
|
|
50
58
|
path: string;
|
|
51
59
|
relative: string;
|
|
52
60
|
rules: Record<string, unknown>;
|
|
@@ -138,7 +146,7 @@ declare const READERS: Record<string, string>;
|
|
|
138
146
|
* block nothing reads. The other four checks ask whether a gate measures what it names; this one
|
|
139
147
|
* asks whether it is still there at all.
|
|
140
148
|
*/
|
|
141
|
-
declare const checkDrift: ({ readers, root, workspaces, }: {
|
|
149
|
+
declare const checkDrift: ({ readers, root, userSettings, workspaces, }: {
|
|
142
150
|
/**
|
|
143
151
|
* Which package reads which block. It is a parameter so a test can name one that can NEVER be
|
|
144
152
|
* installed: a fixture that turns on whether some real package happens to be present on the base
|
|
@@ -147,6 +155,11 @@ declare const checkDrift: ({ readers, root, workspaces, }: {
|
|
|
147
155
|
*/
|
|
148
156
|
readers?: Record<string, string>;
|
|
149
157
|
root: string;
|
|
158
|
+
/**
|
|
159
|
+
* The machine-wide settings, a parameter so a test never asks about the home directory it happens
|
|
160
|
+
* to run in — the same reason `readers` is one.
|
|
161
|
+
*/
|
|
162
|
+
userSettings?: string;
|
|
150
163
|
workspaces: Workspace[];
|
|
151
164
|
}) => Finding[];
|
|
152
165
|
|
|
@@ -244,15 +257,6 @@ declare const repoCorpusOf: (root: string) => string | undefined;
|
|
|
244
257
|
declare const formatDoctor: ({ counts, findings, ok, root }: DoctorReport) => string;
|
|
245
258
|
declare const formatJson: (report: DoctorReport) => string;
|
|
246
259
|
|
|
247
|
-
/**
|
|
248
|
-
* What oxlint would load, asked the only way that answers on every package manager.
|
|
249
|
-
*
|
|
250
|
-
* oxlint resolves a `jsPlugins` specifier from the CONFIG FILE's directory, not the working
|
|
251
|
-
* directory, so the question is asked from a directory and never from the process's cwd. And it is
|
|
252
|
-
* asked of the RESOLVER: counting copies with `find` is a bun/npm check only — on pnpm the entries
|
|
253
|
-
* under `node_modules/@scope/` are symlinks into `.pnpm/`, which keeps every version ever
|
|
254
|
-
* installed, so the same count answers 7 on a correct tree and 0 with the store excluded.
|
|
255
|
-
*/
|
|
256
260
|
declare const resolveFrom: (dir: string, specifier: string) => string;
|
|
257
261
|
/** The package a resolved entry point belongs to: up until a manifest claims the name. */
|
|
258
262
|
declare const packageDirOf: (entry: string, name: string) => string;
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geonosis/doctor",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"types": "./dist/index.d.ts",
|
|
5
5
|
"description": "The adoption doctor — declared ≠ loaded, enabled ≠ exercised, a baseline that grew, a runner whose exit code is the only verdict.",
|
|
6
6
|
"keywords": [
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"dist"
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@geonosis/lint-parity": "1.
|
|
38
|
+
"@geonosis/lint-parity": "1.3.0"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"oxlint": ">=1.77"
|