@geonosis/doctor 1.0.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/LICENSE +202 -0
- package/README.md +167 -0
- package/bin/geonosis-doctor.mjs +4 -0
- package/dist/chunk-R4AHDFE3.js +1122 -0
- package/dist/doctor-cli.js +109 -0
- package/dist/index.d.ts +255 -0
- package/dist/index.js +68 -0
- package/package.json +49 -0
|
@@ -0,0 +1,1122 @@
|
|
|
1
|
+
// src/discover.ts
|
|
2
|
+
import { readdirSync, readFileSync, statSync } from "fs";
|
|
3
|
+
import { join, relative as relativeTo, sep } from "path";
|
|
4
|
+
var CONFIG_FILE = ".oxlintrc.json";
|
|
5
|
+
var MANIFEST_FILE = "package.json";
|
|
6
|
+
var RATCHET_FILE = "geonosis.ratchet.json";
|
|
7
|
+
var NEVER_WALKED = /* @__PURE__ */ new Set(["build", "coverage", "dist", "node_modules", "storybook-static"]);
|
|
8
|
+
var skipped = (name) => name.startsWith(".") || NEVER_WALKED.has(name);
|
|
9
|
+
var relativePath = (root, path) => relativeTo(root, path).split(sep).join("/");
|
|
10
|
+
var walk = (dir, onFile) => {
|
|
11
|
+
let entries;
|
|
12
|
+
try {
|
|
13
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
14
|
+
} catch {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
if (entry.isDirectory()) {
|
|
19
|
+
if (!skipped(entry.name)) walk(join(dir, entry.name), onFile);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (entry.isFile()) onFile(join(dir, entry.name), entry.name);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
var parse = (path) => JSON.parse(readFileSync(path, "utf8"));
|
|
26
|
+
var readConfig = (path, root) => {
|
|
27
|
+
const dir = join(path, "..");
|
|
28
|
+
const relative = relativePath(root, path);
|
|
29
|
+
try {
|
|
30
|
+
const config = parse(path);
|
|
31
|
+
const jsPlugins = Array.isArray(config.jsPlugins) ? config.jsPlugins.filter((one) => typeof one === "string") : [];
|
|
32
|
+
const rules = typeof config.rules === "object" && config.rules !== null ? config.rules : {};
|
|
33
|
+
return { dir, jsPlugins, path, relative, rules };
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return {
|
|
36
|
+
dir,
|
|
37
|
+
error: `could not read it: ${error.message}`,
|
|
38
|
+
jsPlugins: [],
|
|
39
|
+
path,
|
|
40
|
+
relative,
|
|
41
|
+
rules: {}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var discoverConfigs = (root) => {
|
|
46
|
+
const found = [];
|
|
47
|
+
walk(root, (path, name) => {
|
|
48
|
+
if (name === CONFIG_FILE) found.push(readConfig(path, root));
|
|
49
|
+
});
|
|
50
|
+
return found.toSorted((a, b) => a.relative.localeCompare(b.relative));
|
|
51
|
+
};
|
|
52
|
+
var discoverWorkspaces = (root) => {
|
|
53
|
+
const found = [];
|
|
54
|
+
walk(root, (path, name) => {
|
|
55
|
+
if (name !== MANIFEST_FILE) return;
|
|
56
|
+
let manifest;
|
|
57
|
+
try {
|
|
58
|
+
manifest = parse(path);
|
|
59
|
+
} catch {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
found.push({ dir: join(path, ".."), manifest, relative: relativePath(root, join(path, "..")) });
|
|
63
|
+
});
|
|
64
|
+
return found.toSorted((a, b) => a.relative.localeCompare(b.relative));
|
|
65
|
+
};
|
|
66
|
+
var readRatchet = (root) => {
|
|
67
|
+
const path = join(root, RATCHET_FILE);
|
|
68
|
+
try {
|
|
69
|
+
statSync(path);
|
|
70
|
+
} catch {
|
|
71
|
+
return void 0;
|
|
72
|
+
}
|
|
73
|
+
const config = parse(path);
|
|
74
|
+
return {
|
|
75
|
+
baseline: typeof config.baseline === "string" ? config.baseline : "gate-baseline.json",
|
|
76
|
+
counters: Array.isArray(config.counters) ? config.counters : []
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// src/baseline.ts
|
|
81
|
+
import { spawnSync } from "child_process";
|
|
82
|
+
var DEFAULT_REF = "origin/main";
|
|
83
|
+
var git = (root, args) => {
|
|
84
|
+
const run = spawnSync("git", args, { cwd: root, encoding: "utf8" });
|
|
85
|
+
return { code: run.status ?? -1, output: `${run.stdout ?? ""}${run.stderr ?? ""}` };
|
|
86
|
+
};
|
|
87
|
+
var defaultRef = (root) => git(root, ["rev-parse", "--verify", "--quiet", DEFAULT_REF]).code === 0 ? DEFAULT_REF : void 0;
|
|
88
|
+
var numbersAt = (root, ref, path) => {
|
|
89
|
+
const shown = git(root, ["show", `${ref}:${path}`]);
|
|
90
|
+
if (shown.code !== 0) return void 0;
|
|
91
|
+
let parsed;
|
|
92
|
+
try {
|
|
93
|
+
parsed = JSON.parse(shown.output);
|
|
94
|
+
} catch {
|
|
95
|
+
return void 0;
|
|
96
|
+
}
|
|
97
|
+
const numbers = {};
|
|
98
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
99
|
+
if (typeof value === "number") numbers[key] = value;
|
|
100
|
+
}
|
|
101
|
+
return numbers;
|
|
102
|
+
};
|
|
103
|
+
var finding = (subject, verdict, message) => ({
|
|
104
|
+
check: "baseline",
|
|
105
|
+
message,
|
|
106
|
+
subject,
|
|
107
|
+
verdict
|
|
108
|
+
});
|
|
109
|
+
var checkBaseline = ({ ref, root }) => {
|
|
110
|
+
const ratchet = readRatchet(root);
|
|
111
|
+
if (ratchet === void 0) {
|
|
112
|
+
return [
|
|
113
|
+
finding(
|
|
114
|
+
RATCHET_FILE,
|
|
115
|
+
"SKIP",
|
|
116
|
+
`no ${RATCHET_FILE} at this root \u2014 there is no baseline for a ref to be compared against`
|
|
117
|
+
)
|
|
118
|
+
];
|
|
119
|
+
}
|
|
120
|
+
const path = ratchet.baseline;
|
|
121
|
+
const here = numbersAt(root, "HEAD", path);
|
|
122
|
+
if (here === void 0) {
|
|
123
|
+
return [finding(path, "SKIP", `HEAD has no readable ${path} \u2014 nothing committed to compare`)];
|
|
124
|
+
}
|
|
125
|
+
const there = numbersAt(root, ref, path);
|
|
126
|
+
if (there === void 0) {
|
|
127
|
+
return [
|
|
128
|
+
finding(
|
|
129
|
+
path,
|
|
130
|
+
"SKIP",
|
|
131
|
+
`${ref} has no readable ${path} \u2014 this repo cannot be compared against it`
|
|
132
|
+
)
|
|
133
|
+
];
|
|
134
|
+
}
|
|
135
|
+
const grew = Object.entries(there).filter(([key, was]) => (here[key] ?? was) > was).map(
|
|
136
|
+
([key, was]) => finding(path, "FAIL", `${key} ${was} \u2192 ${here[key] ?? was} (grew against ${ref})`)
|
|
137
|
+
);
|
|
138
|
+
return grew.length > 0 ? grew : [
|
|
139
|
+
finding(
|
|
140
|
+
path,
|
|
141
|
+
"OK",
|
|
142
|
+
`no counter grew against ${ref} (${Object.keys(there).length} compared)`
|
|
143
|
+
)
|
|
144
|
+
];
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// src/types.ts
|
|
148
|
+
var CHECKS = [
|
|
149
|
+
"loaded",
|
|
150
|
+
"exercised",
|
|
151
|
+
"baseline",
|
|
152
|
+
"runner",
|
|
153
|
+
"drift",
|
|
154
|
+
"observability"
|
|
155
|
+
];
|
|
156
|
+
var DoctorError = class extends Error {
|
|
157
|
+
constructor(message) {
|
|
158
|
+
super(message);
|
|
159
|
+
this.name = "DoctorError";
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// src/resolve.ts
|
|
164
|
+
import { existsSync, readFileSync as readFileSync2, realpathSync } from "fs";
|
|
165
|
+
import { createRequire } from "module";
|
|
166
|
+
import { dirname, join as join2 } from "path";
|
|
167
|
+
import { pathToFileURL } from "url";
|
|
168
|
+
var resolveFrom = (dir, specifier) => createRequire(join2(dir, "noop.js")).resolve(specifier);
|
|
169
|
+
var packageDirOf = (entry, name) => {
|
|
170
|
+
let dir = dirname(entry);
|
|
171
|
+
for (; ; ) {
|
|
172
|
+
const manifest = join2(dir, "package.json");
|
|
173
|
+
if (existsSync(manifest)) {
|
|
174
|
+
try {
|
|
175
|
+
const parsed = JSON.parse(readFileSync2(manifest, "utf8"));
|
|
176
|
+
if (parsed.name === name) return dir;
|
|
177
|
+
} catch {
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const parent = dirname(dir);
|
|
181
|
+
if (parent === dir) {
|
|
182
|
+
throw new DoctorError(`${entry} sits under no package.json naming "${name}"`);
|
|
183
|
+
}
|
|
184
|
+
dir = parent;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
var pluginVersionOf = async (entry) => {
|
|
188
|
+
const loaded = await import(pathToFileURL(entry).href);
|
|
189
|
+
const version = loaded.default?.meta?.version;
|
|
190
|
+
if (typeof version !== "string") {
|
|
191
|
+
throw new DoctorError(
|
|
192
|
+
`${entry} exports no default plugin carrying a meta.version \u2014 nothing in it says which version oxlint loaded`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
return version;
|
|
196
|
+
};
|
|
197
|
+
var corpusOfPlugin = (from, specifier) => join2(packageDirOf(resolveFrom(from, specifier), specifier), "corpus");
|
|
198
|
+
var real = (path) => {
|
|
199
|
+
try {
|
|
200
|
+
return realpathSync(path);
|
|
201
|
+
} catch {
|
|
202
|
+
return path;
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
var relativeToRoot = (root, path) => relativePath(real(root), real(path));
|
|
206
|
+
|
|
207
|
+
// src/drift.ts
|
|
208
|
+
import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
|
|
209
|
+
import { join as join3, sep as sep2 } from "path";
|
|
210
|
+
var WORKFLOWS = ".github/workflows";
|
|
211
|
+
var SETTINGS = ".claude/settings.json";
|
|
212
|
+
var GEONOSIS = "geonosis.json";
|
|
213
|
+
var LAW = "CLAUDE.md";
|
|
214
|
+
var CEILING = 200;
|
|
215
|
+
var SWITCHED_OFF = /^\s*if:\s*(?:\$\{\{\s*)?false\b/m;
|
|
216
|
+
var TEST_FILE = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
217
|
+
var finding2 = (subject, verdict, message) => ({
|
|
218
|
+
check: "drift",
|
|
219
|
+
message,
|
|
220
|
+
subject,
|
|
221
|
+
verdict
|
|
222
|
+
});
|
|
223
|
+
var NEVER_WALKED2 = /* @__PURE__ */ new Set(["build", "coverage", "dist", "node_modules", "storybook-static"]);
|
|
224
|
+
var filesUnder = (dir, match) => {
|
|
225
|
+
const found = [];
|
|
226
|
+
const walk2 = (at) => {
|
|
227
|
+
let entries;
|
|
228
|
+
try {
|
|
229
|
+
entries = readdirSync2(at, { withFileTypes: true });
|
|
230
|
+
} catch {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
for (const entry of entries) {
|
|
234
|
+
if (entry.isDirectory()) {
|
|
235
|
+
if (!entry.name.startsWith(".") && !NEVER_WALKED2.has(entry.name)) walk2(join3(at, entry.name));
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (entry.isFile() && match(entry.name)) found.push(join3(at, entry.name));
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
walk2(dir);
|
|
242
|
+
return found;
|
|
243
|
+
};
|
|
244
|
+
var ci = (root) => {
|
|
245
|
+
const dir = join3(root, WORKFLOWS);
|
|
246
|
+
if (!existsSync2(dir)) {
|
|
247
|
+
return [finding2(WORKFLOWS, "SKIP", "there are no workflows here to read")];
|
|
248
|
+
}
|
|
249
|
+
return filesUnder(dir, (name) => name.endsWith(".yml") || name.endsWith(".yaml")).map((path) => {
|
|
250
|
+
const at = relativePath(root, path);
|
|
251
|
+
return SWITCHED_OFF.test(readFileSync3(path, "utf8")) ? finding2(
|
|
252
|
+
at,
|
|
253
|
+
"FAIL",
|
|
254
|
+
"a job or step here is switched off by a condition that can never be true \u2014 every gate downstream of it reports green having run nothing"
|
|
255
|
+
) : finding2(at, "OK", "nothing in it is switched off");
|
|
256
|
+
});
|
|
257
|
+
};
|
|
258
|
+
var holds = (parent, child) => child === parent || child.startsWith(`${parent}${sep2}`);
|
|
259
|
+
var ownersOf = (path, workspaces) => workspaces.filter((one) => holds(one.dir, path)).toSorted((a, b) => b.dir.length - a.dir.length);
|
|
260
|
+
var orphanTests = (root, workspaces) => {
|
|
261
|
+
const orphaned = /* @__PURE__ */ new Map();
|
|
262
|
+
for (const path of filesUnder(root, (name) => TEST_FILE.test(name))) {
|
|
263
|
+
const owners = ownersOf(path, workspaces);
|
|
264
|
+
if (owners.some((one) => typeof one.manifest.scripts?.test === "string")) continue;
|
|
265
|
+
const owner = owners[0];
|
|
266
|
+
if (owner === void 0) continue;
|
|
267
|
+
const at = owner.relative === "" ? "package.json" : `${owner.relative}/package.json`;
|
|
268
|
+
orphaned.set(at, [...orphaned.get(at) ?? [], relativePath(root, path)]);
|
|
269
|
+
}
|
|
270
|
+
if (orphaned.size === 0) {
|
|
271
|
+
return [finding2("test files", "OK", "every test file sits under a workspace that runs tests")];
|
|
272
|
+
}
|
|
273
|
+
return [...orphaned.entries()].map(
|
|
274
|
+
([at, files]) => finding2(
|
|
275
|
+
at,
|
|
276
|
+
"FAIL",
|
|
277
|
+
`${files.length} test file(s) here and no test script to run them \u2014 ${files.slice(0, 3).join(", ")}`
|
|
278
|
+
)
|
|
279
|
+
);
|
|
280
|
+
};
|
|
281
|
+
var readGeonosis = (root) => {
|
|
282
|
+
const path = join3(root, GEONOSIS);
|
|
283
|
+
if (!existsSync2(path)) return void 0;
|
|
284
|
+
try {
|
|
285
|
+
return JSON.parse(readFileSync3(path, "utf8"));
|
|
286
|
+
} catch {
|
|
287
|
+
return void 0;
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
var law = (root, config) => {
|
|
291
|
+
const declared = config?.law ?? {};
|
|
292
|
+
const file = typeof declared.file === "string" ? declared.file : LAW;
|
|
293
|
+
const ceiling = typeof declared.maxLines === "number" ? declared.maxLines : CEILING;
|
|
294
|
+
const path = join3(root, file);
|
|
295
|
+
if (!existsSync2(path)) {
|
|
296
|
+
return [finding2(file, "SKIP", "there is no law file here to measure")];
|
|
297
|
+
}
|
|
298
|
+
const source = readFileSync3(path, "utf8");
|
|
299
|
+
const lines = source.split("\n").length - (source.endsWith("\n") ? 1 : 0);
|
|
300
|
+
return [
|
|
301
|
+
lines > ceiling ? finding2(
|
|
302
|
+
file,
|
|
303
|
+
"WARN",
|
|
304
|
+
`${lines} lines against a ceiling of ${ceiling} \u2014 depth belongs in the skills and in the rules, where it is read`
|
|
305
|
+
) : finding2(file, "OK", `${lines} lines, under the ceiling of ${ceiling}`)
|
|
306
|
+
];
|
|
307
|
+
};
|
|
308
|
+
var hooks = (root) => {
|
|
309
|
+
const path = join3(root, SETTINGS);
|
|
310
|
+
if (!existsSync2(path)) {
|
|
311
|
+
return [
|
|
312
|
+
finding2(
|
|
313
|
+
SETTINGS,
|
|
314
|
+
"WARN",
|
|
315
|
+
"nothing here installs the kit\u2019s plugin, so none of the hooks, agents or skills reach this repo \u2014 the gates run, the method does not"
|
|
316
|
+
)
|
|
317
|
+
];
|
|
318
|
+
}
|
|
319
|
+
const source = readFileSync3(path, "utf8");
|
|
320
|
+
return [
|
|
321
|
+
source.includes("geonosis") ? finding2(SETTINGS, "OK", "it installs the kit\u2019s plugin") : finding2(SETTINGS, "WARN", "it names no geonosis plugin or marketplace")
|
|
322
|
+
];
|
|
323
|
+
};
|
|
324
|
+
var READERS = {
|
|
325
|
+
ledger: "@geonosis/ledger",
|
|
326
|
+
review: "@geonosis/review",
|
|
327
|
+
testbed: "@geonosis/testbed",
|
|
328
|
+
verify: "@geonosis/verify",
|
|
329
|
+
walk: "@geonosis/walk"
|
|
330
|
+
};
|
|
331
|
+
var resolves = (root, name) => {
|
|
332
|
+
try {
|
|
333
|
+
resolveFrom(root, name);
|
|
334
|
+
return true;
|
|
335
|
+
} catch {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
var blocks = (root, config, readers) => {
|
|
340
|
+
if (config === void 0) {
|
|
341
|
+
return [
|
|
342
|
+
finding2(
|
|
343
|
+
GEONOSIS,
|
|
344
|
+
"SKIP",
|
|
345
|
+
"there is no geonosis.json here, so no block says what this repo asks the kit to do"
|
|
346
|
+
)
|
|
347
|
+
];
|
|
348
|
+
}
|
|
349
|
+
return Object.entries(readers).flatMap(([block, name]) => {
|
|
350
|
+
const declared = config[block] !== void 0;
|
|
351
|
+
const installed = resolves(root, name);
|
|
352
|
+
if (declared && !installed) {
|
|
353
|
+
return [
|
|
354
|
+
finding2(
|
|
355
|
+
name,
|
|
356
|
+
"WARN",
|
|
357
|
+
`geonosis.json has a "${block}" block and ${name} is not installed here \u2014 nothing reads it`
|
|
358
|
+
)
|
|
359
|
+
];
|
|
360
|
+
}
|
|
361
|
+
if (!declared && installed) {
|
|
362
|
+
return [
|
|
363
|
+
finding2(
|
|
364
|
+
name,
|
|
365
|
+
"WARN",
|
|
366
|
+
`${name} is installed and geonosis.json has no "${block}" block \u2014 it runs on its defaults, whatever they are`
|
|
367
|
+
)
|
|
368
|
+
];
|
|
369
|
+
}
|
|
370
|
+
return declared ? [finding2(name, "OK", `a "${block}" block, and ${name} to read it`)] : [];
|
|
371
|
+
});
|
|
372
|
+
};
|
|
373
|
+
var pluginDirsConfigured = (root) => {
|
|
374
|
+
const path = join3(root, ".oxlintrc.json");
|
|
375
|
+
if (!existsSync2(path)) return void 0;
|
|
376
|
+
try {
|
|
377
|
+
const parsed = JSON.parse(readFileSync3(path, "utf8"));
|
|
378
|
+
const rule = parsed.rules?.["biological-architecture/no-unregistered-plugin-dir"];
|
|
379
|
+
const options = Array.isArray(rule) ? rule[1] : void 0;
|
|
380
|
+
if (options?.roots === void 0 || typeof options.registry !== "string") return void 0;
|
|
381
|
+
return {
|
|
382
|
+
manifests: options.manifests ?? ["index.ts"],
|
|
383
|
+
registry: options.registry,
|
|
384
|
+
roots: options.roots
|
|
385
|
+
};
|
|
386
|
+
} catch {
|
|
387
|
+
return void 0;
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
var pluginDirs = (root) => {
|
|
391
|
+
const configured = pluginDirsConfigured(root);
|
|
392
|
+
if (configured === void 0) {
|
|
393
|
+
return [
|
|
394
|
+
finding2(
|
|
395
|
+
"plugin directories",
|
|
396
|
+
"SKIP",
|
|
397
|
+
"no-unregistered-plugin-dir is not configured here, so this repo has not said where its integrations live"
|
|
398
|
+
)
|
|
399
|
+
];
|
|
400
|
+
}
|
|
401
|
+
const registryPath = join3(root, configured.registry);
|
|
402
|
+
if (!existsSync2(registryPath)) {
|
|
403
|
+
return [finding2(configured.registry, "FAIL", "the registry the rule names is not there")];
|
|
404
|
+
}
|
|
405
|
+
const registry = readFileSync3(registryPath, "utf8");
|
|
406
|
+
const unreachable = [];
|
|
407
|
+
for (const rootDir of configured.roots) {
|
|
408
|
+
const at = join3(root, rootDir);
|
|
409
|
+
if (!existsSync2(at)) continue;
|
|
410
|
+
for (const entry of readdirSync2(at, { withFileTypes: true })) {
|
|
411
|
+
if (!entry.isDirectory()) continue;
|
|
412
|
+
const hasManifest = configured.manifests.some(
|
|
413
|
+
(name) => existsSync2(join3(at, entry.name, name))
|
|
414
|
+
);
|
|
415
|
+
if (hasManifest && !registry.includes(entry.name)) {
|
|
416
|
+
unreachable.push(`${rootDir}/${entry.name}`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return [
|
|
421
|
+
unreachable.length === 0 ? finding2(
|
|
422
|
+
configured.registry,
|
|
423
|
+
"OK",
|
|
424
|
+
"every directory under the declared roots is named by it"
|
|
425
|
+
) : finding2(
|
|
426
|
+
configured.registry,
|
|
427
|
+
"FAIL",
|
|
428
|
+
`${unreachable.length} directory(ies) it never names: ${unreachable.join(", ")}`
|
|
429
|
+
)
|
|
430
|
+
];
|
|
431
|
+
};
|
|
432
|
+
var checkDrift = ({
|
|
433
|
+
readers = READERS,
|
|
434
|
+
root,
|
|
435
|
+
workspaces
|
|
436
|
+
}) => {
|
|
437
|
+
const config = readGeonosis(root);
|
|
438
|
+
return [
|
|
439
|
+
...ci(root),
|
|
440
|
+
...orphanTests(root, workspaces),
|
|
441
|
+
...pluginDirs(root),
|
|
442
|
+
...law(root, config),
|
|
443
|
+
...hooks(root),
|
|
444
|
+
...blocks(root, config, readers)
|
|
445
|
+
];
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
// src/exercised.ts
|
|
449
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
450
|
+
import { existsSync as existsSync3, mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
451
|
+
import { tmpdir } from "os";
|
|
452
|
+
import { join as join4 } from "path";
|
|
453
|
+
import { corpusOf, readManifest } from "@geonosis/lint-parity";
|
|
454
|
+
var OFF = /* @__PURE__ */ new Set([0, "0", "allow", "off", false]);
|
|
455
|
+
var severityOf = (level) => Array.isArray(level) ? level[0] : level;
|
|
456
|
+
var enabledRulesOf = (rules, plugin) => Object.keys(rules).filter((id) => id.startsWith(`${plugin}/`) && !OFF.has(severityOf(rules[id]))).toSorted();
|
|
457
|
+
var finding3 = (subject, verdict, message) => ({
|
|
458
|
+
check: "exercised",
|
|
459
|
+
message,
|
|
460
|
+
subject,
|
|
461
|
+
verdict
|
|
462
|
+
});
|
|
463
|
+
var LIMIT = 800;
|
|
464
|
+
var refusal = (error) => {
|
|
465
|
+
const said = String(error.message).trim();
|
|
466
|
+
return said.length > LIMIT ? `${said.slice(0, LIMIT)}\u2026` : said;
|
|
467
|
+
};
|
|
468
|
+
var reasonFrom = (config, oxlint) => {
|
|
469
|
+
const dir = mkdtempSync(join4(tmpdir(), "geonosis-doctor-why-"));
|
|
470
|
+
try {
|
|
471
|
+
const probe = join4(dir, "probe.tsx");
|
|
472
|
+
writeFileSync(probe, "export const probe = 1\n");
|
|
473
|
+
const run = spawnSync2(
|
|
474
|
+
oxlint,
|
|
475
|
+
["--no-ignore", "--disable-nested-config", "--config", config, probe],
|
|
476
|
+
{ encoding: "utf8", env: { ...process.env, NO_COLOR: "1" }, maxBuffer: 8 * 1024 * 1024 }
|
|
477
|
+
);
|
|
478
|
+
const said = `${run.stdout ?? ""}${run.stderr ?? ""}`;
|
|
479
|
+
return /Error:\s*([^]+?)(?=\s+at\s+\S+\s+\(|\n|$)/.exec(said)?.[1];
|
|
480
|
+
} catch {
|
|
481
|
+
return void 0;
|
|
482
|
+
} finally {
|
|
483
|
+
rmSync(dir, { force: true, recursive: true });
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
var named = (rules) => rules.join(", ");
|
|
487
|
+
var countOf = (rules, verb) => rules.length === 1 ? `1 enabled rule ${verb}s` : `${rules.length} enabled rules ${verb}`;
|
|
488
|
+
var ownReach = ({
|
|
489
|
+
config,
|
|
490
|
+
oxlint,
|
|
491
|
+
repoCorpus
|
|
492
|
+
}) => {
|
|
493
|
+
const manifest = readManifest(repoCorpus);
|
|
494
|
+
const reach = corpusOf({
|
|
495
|
+
configA: config.path,
|
|
496
|
+
configB: config.path,
|
|
497
|
+
corpus: repoCorpus,
|
|
498
|
+
oxlint
|
|
499
|
+
}).reach;
|
|
500
|
+
return {
|
|
501
|
+
claimed: manifest.rules,
|
|
502
|
+
fired: new Set(reach.filter((one) => one.firedInA).map((one) => one.rule))
|
|
503
|
+
};
|
|
504
|
+
};
|
|
505
|
+
var checkExercised = ({
|
|
506
|
+
config,
|
|
507
|
+
corpus,
|
|
508
|
+
oxlint,
|
|
509
|
+
repoCorpus,
|
|
510
|
+
root
|
|
511
|
+
}) => {
|
|
512
|
+
const said = (verdict, message) => finding3(config.relative, verdict, message);
|
|
513
|
+
if (repoCorpus !== void 0 && !existsSync3(repoCorpus)) {
|
|
514
|
+
return said(
|
|
515
|
+
"FAIL",
|
|
516
|
+
`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`
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
if (!existsSync3(corpus)) {
|
|
520
|
+
return said(
|
|
521
|
+
"SKIP",
|
|
522
|
+
`the plugin loaded from here ships no corpus at ${relativeToRoot(root, corpus)} \u2014 nothing declares which rules it can be evidence about`
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
let manifest;
|
|
526
|
+
try {
|
|
527
|
+
manifest = readManifest(corpus);
|
|
528
|
+
} catch (error) {
|
|
529
|
+
return said("SKIP", refusal(error));
|
|
530
|
+
}
|
|
531
|
+
const enabled = enabledRulesOf(config.rules, manifest.plugin);
|
|
532
|
+
if (enabled.length === 0) return said("SKIP", `no ${manifest.plugin} rule is enabled here`);
|
|
533
|
+
let reach;
|
|
534
|
+
try {
|
|
535
|
+
reach = corpusOf({ configA: config.path, configB: config.path, corpus, oxlint }).reach;
|
|
536
|
+
} catch (error) {
|
|
537
|
+
return said(
|
|
538
|
+
"FAIL",
|
|
539
|
+
`oxlint refused to run this config over the corpus, so nothing here fired at all \u2014 ${reasonFrom(config.path, oxlint) ?? refusal(error)}`
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
const fired = new Set(reach.filter((one) => one.firedInA).map((one) => one.rule));
|
|
543
|
+
const unknown = enabled.filter((rule) => !manifest.rules.includes(rule));
|
|
544
|
+
if (unknown.length > 0) {
|
|
545
|
+
return said(
|
|
546
|
+
"FAIL",
|
|
547
|
+
`${countOf(unknown, "name")} the loaded plugin does not export: ${named(unknown)}`
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
let own = { claimed: [], fired: /* @__PURE__ */ new Set() };
|
|
551
|
+
if (repoCorpus !== void 0) {
|
|
552
|
+
try {
|
|
553
|
+
own = ownReach({ config, oxlint, repoCorpus });
|
|
554
|
+
} catch (error) {
|
|
555
|
+
return said("FAIL", `this repo's own corpus could not be run \u2014 ${refusal(error)}`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
const empty = own.claimed.filter((rule) => !own.fired.has(rule));
|
|
559
|
+
if (empty.length > 0) {
|
|
560
|
+
return said(
|
|
561
|
+
"FAIL",
|
|
562
|
+
`${countOf(empty, "fire")} nowhere in this repo's own corpus, which names them: ${named(empty)}`
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
const silent = enabled.filter((rule) => !fired.has(rule) && !own.fired.has(rule));
|
|
566
|
+
const where = own.claimed.length === 0 ? "" : ` (${own.fired.size} by this repo's own corpus)`;
|
|
567
|
+
return silent.length === 0 ? said("OK", `${enabled.length} enabled, ${enabled.length} exercised${where}`) : said("FAIL", `${countOf(silent, "fire")} nowhere in the corpus: ${named(silent)}`);
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
// src/loaded.ts
|
|
571
|
+
import { sep as sep3 } from "path";
|
|
572
|
+
var SCOPE = "@geonosis/";
|
|
573
|
+
var BLOCKS = [
|
|
574
|
+
"dependencies",
|
|
575
|
+
"devDependencies",
|
|
576
|
+
"optionalDependencies",
|
|
577
|
+
"peerDependencies"
|
|
578
|
+
];
|
|
579
|
+
var RELEASE = /^v?(\d+)\.(\d+)\.(\d+)$/;
|
|
580
|
+
var PINNED = /^[=v]?(\d+\.\d+\.\d+(?:[-+][\w.-]+)?)$/;
|
|
581
|
+
var RANGE = /^(\^|~|>=)\s*v?(\d+)\.(\d+)\.(\d+)$/;
|
|
582
|
+
var LINKED = /^(?:file|link|portal|workspace):/;
|
|
583
|
+
var ANY = /* @__PURE__ */ new Set(["", "*", "latest", "x"]);
|
|
584
|
+
var versionOf = (found, at) => Number(found[at] ?? Number.NaN);
|
|
585
|
+
var parts = (found) => [
|
|
586
|
+
versionOf(found, 1),
|
|
587
|
+
versionOf(found, 2),
|
|
588
|
+
versionOf(found, 3)
|
|
589
|
+
];
|
|
590
|
+
var below = (here, bound) => {
|
|
591
|
+
for (const [at, one] of here.entries()) {
|
|
592
|
+
const other = bound[at] ?? 0;
|
|
593
|
+
if (one !== other) return one < other;
|
|
594
|
+
}
|
|
595
|
+
return false;
|
|
596
|
+
};
|
|
597
|
+
var satisfies = (version, spec) => {
|
|
598
|
+
const wanted = spec.trim();
|
|
599
|
+
if (LINKED.test(wanted) || ANY.has(wanted)) return true;
|
|
600
|
+
const range = RANGE.exec(wanted);
|
|
601
|
+
if (range === null) {
|
|
602
|
+
const pinned = PINNED.exec(wanted);
|
|
603
|
+
return pinned === null ? void 0 : pinned[1] === version.replace(/^v/, "");
|
|
604
|
+
}
|
|
605
|
+
const here = RELEASE.exec(version);
|
|
606
|
+
if (here === null) return void 0;
|
|
607
|
+
const at = parts(here);
|
|
608
|
+
const bound = [
|
|
609
|
+
versionOf(range, 2),
|
|
610
|
+
versionOf(range, 3),
|
|
611
|
+
versionOf(range, 4)
|
|
612
|
+
];
|
|
613
|
+
if (below(at, bound)) return false;
|
|
614
|
+
if (range[1] === ">=") return true;
|
|
615
|
+
if (range[1] === "~") return at[0] === bound[0] && at[1] === bound[1];
|
|
616
|
+
if (bound[0] > 0) return at[0] === bound[0];
|
|
617
|
+
if (bound[1] > 0) return at[0] === 0 && at[1] === bound[1];
|
|
618
|
+
return at[0] === 0 && at[1] === 0 && at[2] === bound[2];
|
|
619
|
+
};
|
|
620
|
+
var specIn = (manifest, specifier) => {
|
|
621
|
+
for (const block of BLOCKS) {
|
|
622
|
+
const spec = manifest[block]?.[specifier];
|
|
623
|
+
if (typeof spec === "string") return spec;
|
|
624
|
+
}
|
|
625
|
+
return void 0;
|
|
626
|
+
};
|
|
627
|
+
var holds2 = (parent, child) => child === parent || child.startsWith(`${parent}${sep3}`);
|
|
628
|
+
var manifestNameOf = (workspace) => workspace.relative === "" ? "package.json" : `${workspace.relative}/package.json`;
|
|
629
|
+
var declaredFor = ({
|
|
630
|
+
dir,
|
|
631
|
+
specifier,
|
|
632
|
+
workspaces
|
|
633
|
+
}) => {
|
|
634
|
+
const upwards = workspaces.filter((one) => holds2(one.dir, dir)).toSorted((a, b) => b.dir.length - a.dir.length);
|
|
635
|
+
for (const workspace of upwards) {
|
|
636
|
+
const spec = specIn(workspace.manifest, specifier);
|
|
637
|
+
if (spec !== void 0) return { at: manifestNameOf(workspace), spec };
|
|
638
|
+
}
|
|
639
|
+
return void 0;
|
|
640
|
+
};
|
|
641
|
+
var finding4 = (subject, verdict, message) => ({
|
|
642
|
+
check: "loaded",
|
|
643
|
+
message,
|
|
644
|
+
subject,
|
|
645
|
+
verdict
|
|
646
|
+
});
|
|
647
|
+
var firstLine = (error) => String(error.message).split("\n")[0] ?? "";
|
|
648
|
+
var versionAt = async (entry, specifier, root) => ({
|
|
649
|
+
at: relativeToRoot(root, packageDirOf(entry, specifier)),
|
|
650
|
+
version: await pluginVersionOf(entry)
|
|
651
|
+
});
|
|
652
|
+
var oneConfig = async ({
|
|
653
|
+
config,
|
|
654
|
+
root,
|
|
655
|
+
specifier,
|
|
656
|
+
workspaces
|
|
657
|
+
}) => {
|
|
658
|
+
const said = (verdict, message) => finding4(config.relative, verdict, `${specifier}: ${message}`);
|
|
659
|
+
let loaded;
|
|
660
|
+
try {
|
|
661
|
+
loaded = await versionAt(resolveFrom(config.dir, specifier), specifier, root);
|
|
662
|
+
} catch (error) {
|
|
663
|
+
return said(
|
|
664
|
+
"FAIL",
|
|
665
|
+
`could not resolve it from this config's directory, and oxlint resolves it from exactly there \u2014 ${firstLine(error)}`
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
const declared = declaredFor({ dir: config.dir, specifier, workspaces });
|
|
669
|
+
if (declared === void 0) {
|
|
670
|
+
return said(
|
|
671
|
+
"FAIL",
|
|
672
|
+
`loaded ${loaded.version} from ${loaded.at}, and no package.json from here up to the root declares it \u2014 nothing says which version this was meant to be`
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
const held = satisfies(loaded.version, declared.spec);
|
|
676
|
+
if (held === void 0) {
|
|
677
|
+
return said(
|
|
678
|
+
"FAIL",
|
|
679
|
+
`loaded ${loaded.version}, and ${declared.at} declares "${declared.spec}" \u2014 a range this check cannot read, so it will not call the version right`
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
return held ? said("OK", `loaded ${loaded.version} = declared ${declared.spec} (${declared.at})`) : said(
|
|
683
|
+
"FAIL",
|
|
684
|
+
`loaded ${loaded.version}, declared ${declared.spec} (${declared.at}) \u2014 a nested copy at ${loaded.at}`
|
|
685
|
+
);
|
|
686
|
+
};
|
|
687
|
+
var labelOf = (workspace) => workspace.relative === "" ? "root" : workspace.relative;
|
|
688
|
+
var copiesOf = async ({
|
|
689
|
+
root,
|
|
690
|
+
specifier,
|
|
691
|
+
workspaces
|
|
692
|
+
}) => {
|
|
693
|
+
const found = /* @__PURE__ */ new Map();
|
|
694
|
+
for (const workspace of workspaces) {
|
|
695
|
+
let entry;
|
|
696
|
+
try {
|
|
697
|
+
entry = resolveFrom(workspace.dir, specifier);
|
|
698
|
+
} catch {
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
const at = relativeToRoot(root, packageDirOf(entry, specifier));
|
|
702
|
+
const already = found.get(at);
|
|
703
|
+
if (already !== void 0) {
|
|
704
|
+
already.from.push(labelOf(workspace));
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
found.set(at, { from: [labelOf(workspace)], version: await pluginVersionOf(entry) });
|
|
708
|
+
}
|
|
709
|
+
if (found.size === 0) {
|
|
710
|
+
return finding4(specifier, "FAIL", "no workspace in this tree can resolve it at all");
|
|
711
|
+
}
|
|
712
|
+
const listed = [...found.entries()].map(([at, one]) => `${at} ${one.version} (${one.from.join(", ")})`).join("; ");
|
|
713
|
+
return found.size === 1 ? finding4(specifier, "OK", `1 copy \u2014 ${listed}`) : finding4(
|
|
714
|
+
specifier,
|
|
715
|
+
"WARN",
|
|
716
|
+
`${found.size} copies \u2014 ${listed}. Which one oxlint runs depends on which directory its config sits in.`
|
|
717
|
+
);
|
|
718
|
+
};
|
|
719
|
+
var checkLoaded = async ({
|
|
720
|
+
configs,
|
|
721
|
+
root,
|
|
722
|
+
workspaces
|
|
723
|
+
}) => {
|
|
724
|
+
const findings = [];
|
|
725
|
+
const specifiers = /* @__PURE__ */ new Set();
|
|
726
|
+
for (const config of configs) {
|
|
727
|
+
if (config.error !== void 0) {
|
|
728
|
+
findings.push(finding4(config.relative, "FAIL", config.error));
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
for (const specifier of config.jsPlugins.filter((name) => name.startsWith(SCOPE))) {
|
|
732
|
+
specifiers.add(specifier);
|
|
733
|
+
findings.push(await oneConfig({ config, root, specifier, workspaces }));
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
for (const specifier of [...specifiers].toSorted()) {
|
|
737
|
+
findings.push(await copiesOf({ root, specifier, workspaces }));
|
|
738
|
+
}
|
|
739
|
+
return findings;
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
// src/observability.ts
|
|
743
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
744
|
+
import { join as join5 } from "path";
|
|
745
|
+
var GEONOSIS_FILE = "geonosis.json";
|
|
746
|
+
var REACHES_NOTHING = /* @__PURE__ */ new Set(["console", "memory", "noop", "none", "null", "swallowing"]);
|
|
747
|
+
var DEFAULT_MAX_AGE_SECONDS = 3600;
|
|
748
|
+
var HEAD_TIMEOUT_MS = 3e3;
|
|
749
|
+
var finding5 = (verdict, subject, message) => ({
|
|
750
|
+
check: "observability",
|
|
751
|
+
message,
|
|
752
|
+
subject,
|
|
753
|
+
verdict
|
|
754
|
+
});
|
|
755
|
+
var readGeonosis2 = (root) => {
|
|
756
|
+
let text;
|
|
757
|
+
try {
|
|
758
|
+
text = readFileSync4(join5(root, GEONOSIS_FILE), "utf8");
|
|
759
|
+
} catch {
|
|
760
|
+
return { present: false };
|
|
761
|
+
}
|
|
762
|
+
try {
|
|
763
|
+
const parsed = JSON.parse(text);
|
|
764
|
+
const block = parsed.observability;
|
|
765
|
+
if (typeof block !== "object" || block === null || Array.isArray(block)) {
|
|
766
|
+
return { present: false };
|
|
767
|
+
}
|
|
768
|
+
return { config: block, present: true };
|
|
769
|
+
} catch (error) {
|
|
770
|
+
return { error: error.message, present: false };
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
var exporterFinding = (config) => {
|
|
774
|
+
const sink = config.sink;
|
|
775
|
+
if (typeof sink !== "string" || sink.trim() === "") {
|
|
776
|
+
return finding5(
|
|
777
|
+
"FAIL",
|
|
778
|
+
GEONOSIS_FILE,
|
|
779
|
+
"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"
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
if (REACHES_NOTHING.has(sink.toLowerCase())) {
|
|
783
|
+
return finding5(
|
|
784
|
+
"WARN",
|
|
785
|
+
GEONOSIS_FILE,
|
|
786
|
+
`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.`
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
return finding5("OK", GEONOSIS_FILE, `the configured sink is "${sink}"`);
|
|
790
|
+
};
|
|
791
|
+
var reachableFinding = async (config) => {
|
|
792
|
+
const endpoint = config.endpoint;
|
|
793
|
+
if (typeof endpoint !== "string" || endpoint.trim() === "") {
|
|
794
|
+
return finding5(
|
|
795
|
+
"SKIP",
|
|
796
|
+
GEONOSIS_FILE,
|
|
797
|
+
"no observability.endpoint was named, so whether the exporter is reachable was not asked"
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
const controller = new AbortController();
|
|
801
|
+
const timer = setTimeout(() => controller.abort(), HEAD_TIMEOUT_MS);
|
|
802
|
+
try {
|
|
803
|
+
const response = await fetch(endpoint, { method: "HEAD", signal: controller.signal });
|
|
804
|
+
return finding5(
|
|
805
|
+
"OK",
|
|
806
|
+
GEONOSIS_FILE,
|
|
807
|
+
`${endpoint} is reachable \u2014 it answered ${response.status} to a HEAD`
|
|
808
|
+
);
|
|
809
|
+
} catch (error) {
|
|
810
|
+
return finding5(
|
|
811
|
+
"FAIL",
|
|
812
|
+
GEONOSIS_FILE,
|
|
813
|
+
`${endpoint} is not reachable from here: ${error.message}. Every report this repo sends is going into that.`
|
|
814
|
+
);
|
|
815
|
+
} finally {
|
|
816
|
+
clearTimeout(timer);
|
|
817
|
+
}
|
|
818
|
+
};
|
|
819
|
+
var ageFinding = (config, root, now) => {
|
|
820
|
+
const file = config.lastEventFile;
|
|
821
|
+
if (typeof file !== "string" || file.trim() === "") {
|
|
822
|
+
return finding5(
|
|
823
|
+
"SKIP",
|
|
824
|
+
GEONOSIS_FILE,
|
|
825
|
+
"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."
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
const maxAgeSeconds = typeof config.maxAgeSeconds === "number" && config.maxAgeSeconds > 0 ? config.maxAgeSeconds : DEFAULT_MAX_AGE_SECONDS;
|
|
829
|
+
let record;
|
|
830
|
+
try {
|
|
831
|
+
record = JSON.parse(readFileSync4(join5(root, file), "utf8"));
|
|
832
|
+
} catch (error) {
|
|
833
|
+
return finding5(
|
|
834
|
+
"FAIL",
|
|
835
|
+
file,
|
|
836
|
+
`the last event file could not be read: ${error.message}. A sink that has never written one has never captured anything.`
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
if (typeof record.at !== "number" || !Number.isFinite(record.at)) {
|
|
840
|
+
return finding5(
|
|
841
|
+
"FAIL",
|
|
842
|
+
file,
|
|
843
|
+
'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'
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
const ageSeconds = Math.round((now - record.at) / 1e3);
|
|
847
|
+
return ageSeconds > maxAgeSeconds ? finding5(
|
|
848
|
+
"FAIL",
|
|
849
|
+
file,
|
|
850
|
+
`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.`
|
|
851
|
+
) : finding5(
|
|
852
|
+
"OK",
|
|
853
|
+
file,
|
|
854
|
+
`the last event arrived ${ageSeconds}s ago, inside the ${maxAgeSeconds}s window`
|
|
855
|
+
);
|
|
856
|
+
};
|
|
857
|
+
var probeFinding = (config) => {
|
|
858
|
+
const probe = config.probe;
|
|
859
|
+
if (typeof probe === "string" && probe.trim() !== "") {
|
|
860
|
+
return finding5("OK", GEONOSIS_FILE, `the probe that proves this exporter is "${probe}"`);
|
|
861
|
+
}
|
|
862
|
+
if (typeof config.lastEventFile === "string" && config.lastEventFile.trim() !== "") {
|
|
863
|
+
return finding5(
|
|
864
|
+
"OK",
|
|
865
|
+
GEONOSIS_FILE,
|
|
866
|
+
"no probe command, but a last event file is read above, so something does look at this exporter"
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
return finding5(
|
|
870
|
+
"WARN",
|
|
871
|
+
GEONOSIS_FILE,
|
|
872
|
+
"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."
|
|
873
|
+
);
|
|
874
|
+
};
|
|
875
|
+
var checkObservability = async ({
|
|
876
|
+
now,
|
|
877
|
+
root
|
|
878
|
+
}) => {
|
|
879
|
+
const read = readGeonosis2(root);
|
|
880
|
+
if (read.error !== void 0) {
|
|
881
|
+
return [
|
|
882
|
+
finding5(
|
|
883
|
+
"FAIL",
|
|
884
|
+
GEONOSIS_FILE,
|
|
885
|
+
`${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.`
|
|
886
|
+
)
|
|
887
|
+
];
|
|
888
|
+
}
|
|
889
|
+
if (!read.present || read.config === void 0) {
|
|
890
|
+
return [
|
|
891
|
+
finding5(
|
|
892
|
+
"SKIP",
|
|
893
|
+
GEONOSIS_FILE,
|
|
894
|
+
`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.`
|
|
895
|
+
)
|
|
896
|
+
];
|
|
897
|
+
}
|
|
898
|
+
const config = read.config;
|
|
899
|
+
return [
|
|
900
|
+
exporterFinding(config),
|
|
901
|
+
await reachableFinding(config),
|
|
902
|
+
ageFinding(config, root, now),
|
|
903
|
+
probeFinding(config)
|
|
904
|
+
];
|
|
905
|
+
};
|
|
906
|
+
|
|
907
|
+
// src/repo-corpus.ts
|
|
908
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
|
|
909
|
+
import { join as join6 } from "path";
|
|
910
|
+
var GEONOSIS_FILE2 = "geonosis.json";
|
|
911
|
+
var repoCorpusOf = (root) => {
|
|
912
|
+
const path = join6(root, GEONOSIS_FILE2);
|
|
913
|
+
if (!existsSync4(path)) return void 0;
|
|
914
|
+
try {
|
|
915
|
+
const parsed = JSON.parse(readFileSync5(path, "utf8"));
|
|
916
|
+
const declared = parsed.doctor?.corpus;
|
|
917
|
+
return typeof declared === "string" && declared !== "" ? join6(root, declared) : void 0;
|
|
918
|
+
} catch {
|
|
919
|
+
return void 0;
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
// src/runner.ts
|
|
924
|
+
var TEST_FAILURES = "testFailures";
|
|
925
|
+
var RUNS_A_RUNNER = /(?:^|[\s;&|(])(?:npx\s+|bunx\s+|pnpm\s+(?:exec\s+)?)?(?:vitest|bun\s+test)(?:\s|$)/;
|
|
926
|
+
var WRITES_A_REPORT = /--reporter[= ]\S*json|--outputFile/i;
|
|
927
|
+
var finding6 = (subject, verdict, message) => ({
|
|
928
|
+
check: "runner",
|
|
929
|
+
message,
|
|
930
|
+
subject,
|
|
931
|
+
verdict
|
|
932
|
+
});
|
|
933
|
+
var stringOf = (value) => typeof value === "string" ? value : "";
|
|
934
|
+
var namesOf = (workspace) => [workspace.relative, stringOf(workspace.manifest.name)].filter((one) => one !== "");
|
|
935
|
+
var covers = (command, workspace, workspaces) => {
|
|
936
|
+
if (workspace.relative !== "") return namesOf(workspace).some((one) => command.includes(one));
|
|
937
|
+
return !workspaces.some(
|
|
938
|
+
(other) => other.relative !== "" && namesOf(other).some((one) => command.includes(one))
|
|
939
|
+
);
|
|
940
|
+
};
|
|
941
|
+
var reportingCounters = (ratchet) => (ratchet?.counters ?? []).filter(
|
|
942
|
+
(entry) => entry.counter === TEST_FAILURES && stringOf(entry.report) !== ""
|
|
943
|
+
);
|
|
944
|
+
var keyOf = (entry) => stringOf(entry.key) === "" ? TEST_FAILURES : stringOf(entry.key);
|
|
945
|
+
var checkRunner = ({
|
|
946
|
+
ratchet,
|
|
947
|
+
workspaces
|
|
948
|
+
}) => {
|
|
949
|
+
const reading = reportingCounters(ratchet);
|
|
950
|
+
return workspaces.flatMap((workspace) => {
|
|
951
|
+
const script = stringOf(workspace.manifest.scripts?.test);
|
|
952
|
+
if (script === "") return [];
|
|
953
|
+
const subject = workspace.relative === "" ? "package.json" : `${workspace.relative}/package.json`;
|
|
954
|
+
const said = (verdict, message) => [
|
|
955
|
+
finding6(subject, verdict, message)
|
|
956
|
+
];
|
|
957
|
+
if (!RUNS_A_RUNNER.test(script)) {
|
|
958
|
+
return said(
|
|
959
|
+
"OK",
|
|
960
|
+
`"${script}" runs neither vitest nor bun test \u2014 this check has nothing to say about it`
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
if (WRITES_A_REPORT.test(script)) {
|
|
964
|
+
return said("OK", "the script asks the runner for its own JSON report, not for a status code");
|
|
965
|
+
}
|
|
966
|
+
const counter = reading.find((entry) => covers(stringOf(entry.command), workspace, workspaces));
|
|
967
|
+
if (counter !== void 0) {
|
|
968
|
+
return said("OK", `read by the ratchet's "${keyOf(counter)}" counter in report mode`);
|
|
969
|
+
}
|
|
970
|
+
return said(
|
|
971
|
+
"WARN",
|
|
972
|
+
`"${script}" \u2014 the test runner's exit code is the only verdict here; vitest-pool-workers exited 0 on failing tests for weeks in a consumer. Give the ratchet a ${TEST_FAILURES} counter in report mode covering this workspace, or have the script write a JSON report.`
|
|
973
|
+
);
|
|
974
|
+
});
|
|
975
|
+
};
|
|
976
|
+
|
|
977
|
+
// src/doctor.ts
|
|
978
|
+
import { resolveOxlint } from "@geonosis/lint-parity";
|
|
979
|
+
var exercisedOf = ({
|
|
980
|
+
configs,
|
|
981
|
+
oxlint,
|
|
982
|
+
repoCorpus,
|
|
983
|
+
root
|
|
984
|
+
}) => configs.flatMap(
|
|
985
|
+
(config) => config.jsPlugins.filter((name) => name.startsWith(SCOPE)).map((specifier) => {
|
|
986
|
+
let corpus;
|
|
987
|
+
try {
|
|
988
|
+
corpus = corpusOfPlugin(config.dir, specifier);
|
|
989
|
+
} catch (error) {
|
|
990
|
+
return {
|
|
991
|
+
check: "exercised",
|
|
992
|
+
message: `${specifier}: ${String(error.message)}`,
|
|
993
|
+
subject: config.relative,
|
|
994
|
+
verdict: "SKIP"
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
const own = repoCorpus !== void 0 && config.relative === CONFIG_FILE ? repoCorpus : void 0;
|
|
998
|
+
return checkExercised({
|
|
999
|
+
config,
|
|
1000
|
+
corpus,
|
|
1001
|
+
oxlint,
|
|
1002
|
+
...own === void 0 ? {} : { repoCorpus: own },
|
|
1003
|
+
root
|
|
1004
|
+
});
|
|
1005
|
+
})
|
|
1006
|
+
);
|
|
1007
|
+
var skip = (message) => [
|
|
1008
|
+
{ check: "baseline", message, subject: RATCHET_FILE, verdict: "SKIP" }
|
|
1009
|
+
];
|
|
1010
|
+
var baselineOf = ({
|
|
1011
|
+
baseline,
|
|
1012
|
+
root
|
|
1013
|
+
}) => {
|
|
1014
|
+
if (baseline === void 0) {
|
|
1015
|
+
return skip(
|
|
1016
|
+
"not requested \u2014 pass --baseline-against [ref] to compare this baseline against another ref (it is the one check that runs git)"
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
const ref = baseline.ref ?? defaultRef(root);
|
|
1020
|
+
return ref === void 0 ? skip("no ref was named and this repo has no origin/main to fall back on") : checkBaseline({ ref, root });
|
|
1021
|
+
};
|
|
1022
|
+
var ordered = (findings) => CHECKS.flatMap((check) => findings.filter((one) => one.check === check));
|
|
1023
|
+
var EMPTY = { FAIL: 0, OK: 0, SKIP: 0, WARN: 0 };
|
|
1024
|
+
var countsOf = (findings) => findings.reduce((counts, one) => ({ ...counts, [one.verdict]: counts[one.verdict] + 1 }), {
|
|
1025
|
+
...EMPTY
|
|
1026
|
+
});
|
|
1027
|
+
var runDoctor = async ({
|
|
1028
|
+
baseline,
|
|
1029
|
+
oxlint,
|
|
1030
|
+
root,
|
|
1031
|
+
strict = false
|
|
1032
|
+
}) => {
|
|
1033
|
+
const configs = discoverConfigs(root);
|
|
1034
|
+
const workspaces = discoverWorkspaces(root);
|
|
1035
|
+
const binary = oxlint ?? resolveOxlint(root);
|
|
1036
|
+
const repoCorpus = repoCorpusOf(root);
|
|
1037
|
+
const found = ordered([
|
|
1038
|
+
...await checkLoaded({ configs, root, workspaces }),
|
|
1039
|
+
...exercisedOf({
|
|
1040
|
+
configs,
|
|
1041
|
+
oxlint: binary,
|
|
1042
|
+
...repoCorpus === void 0 ? {} : { repoCorpus },
|
|
1043
|
+
root
|
|
1044
|
+
}),
|
|
1045
|
+
...baselineOf({ baseline, root }),
|
|
1046
|
+
...checkRunner({ ratchet: readRatchet(root), workspaces }),
|
|
1047
|
+
...await checkObservability({ now: Date.now(), root }),
|
|
1048
|
+
...checkDrift({ root, workspaces })
|
|
1049
|
+
]);
|
|
1050
|
+
const findings = strict ? found.map((one) => one.verdict === "WARN" ? { ...one, verdict: "FAIL" } : one) : found;
|
|
1051
|
+
return {
|
|
1052
|
+
counts: countsOf(findings),
|
|
1053
|
+
findings,
|
|
1054
|
+
ok: !findings.some((one) => one.verdict === "FAIL"),
|
|
1055
|
+
root
|
|
1056
|
+
};
|
|
1057
|
+
};
|
|
1058
|
+
|
|
1059
|
+
// src/report.ts
|
|
1060
|
+
var ABOUT = {
|
|
1061
|
+
baseline: "a number that may only shrink, against another ref",
|
|
1062
|
+
drift: "the gates that were set up and are no longer running",
|
|
1063
|
+
exercised: "every enabled rule fires on at least one corpus file",
|
|
1064
|
+
loaded: "the plugin oxlint would load is the one the manifest pins",
|
|
1065
|
+
observability: "an exporter is configured, reachable, and something arrived through it lately",
|
|
1066
|
+
runner: "something reads the test runner\u2019s own report, not its exit code"
|
|
1067
|
+
};
|
|
1068
|
+
var WIDTH = 4;
|
|
1069
|
+
var lineOf = (one) => ` ${one.verdict.padEnd(WIDTH)} ${one.subject}: ${one.message}`;
|
|
1070
|
+
var sectionOf = (check, findings) => {
|
|
1071
|
+
const mine = findings.filter((one) => one.check === check);
|
|
1072
|
+
return [
|
|
1073
|
+
`${check} \u2014 ${ABOUT[check]}`,
|
|
1074
|
+
...mine.length === 0 ? [" ---- nothing in this tree to check"] : mine.map(lineOf),
|
|
1075
|
+
""
|
|
1076
|
+
];
|
|
1077
|
+
};
|
|
1078
|
+
var formatDoctor = ({ counts, findings, ok, root }) => [
|
|
1079
|
+
`geonosis-doctor \u2014 ${root}`,
|
|
1080
|
+
"",
|
|
1081
|
+
...CHECKS.flatMap((check) => sectionOf(check, findings)),
|
|
1082
|
+
`${CHECKS.length} checks, ${findings.length} lines: ${counts.OK} ok, ${counts.WARN} warned, ${counts.SKIP} skipped, ${counts.FAIL} failed`,
|
|
1083
|
+
ok ? "doctor PASS \u2014 nothing here says the gates are measuring something other than what they claim." : "doctor FAIL \u2014 a line above is a gate reporting on something other than what it names.",
|
|
1084
|
+
""
|
|
1085
|
+
].join("\n");
|
|
1086
|
+
var formatJson = (report) => `${JSON.stringify(report, null, 2)}
|
|
1087
|
+
`;
|
|
1088
|
+
|
|
1089
|
+
export {
|
|
1090
|
+
CONFIG_FILE,
|
|
1091
|
+
MANIFEST_FILE,
|
|
1092
|
+
RATCHET_FILE,
|
|
1093
|
+
relativePath,
|
|
1094
|
+
readConfig,
|
|
1095
|
+
discoverConfigs,
|
|
1096
|
+
discoverWorkspaces,
|
|
1097
|
+
readRatchet,
|
|
1098
|
+
defaultRef,
|
|
1099
|
+
checkBaseline,
|
|
1100
|
+
CHECKS,
|
|
1101
|
+
DoctorError,
|
|
1102
|
+
resolveFrom,
|
|
1103
|
+
packageDirOf,
|
|
1104
|
+
pluginVersionOf,
|
|
1105
|
+
corpusOfPlugin,
|
|
1106
|
+
relativeToRoot,
|
|
1107
|
+
READERS,
|
|
1108
|
+
checkDrift,
|
|
1109
|
+
enabledRulesOf,
|
|
1110
|
+
checkExercised,
|
|
1111
|
+
SCOPE,
|
|
1112
|
+
satisfies,
|
|
1113
|
+
declaredFor,
|
|
1114
|
+
checkLoaded,
|
|
1115
|
+
checkObservability,
|
|
1116
|
+
GEONOSIS_FILE2 as GEONOSIS_FILE,
|
|
1117
|
+
repoCorpusOf,
|
|
1118
|
+
checkRunner,
|
|
1119
|
+
runDoctor,
|
|
1120
|
+
formatDoctor,
|
|
1121
|
+
formatJson
|
|
1122
|
+
};
|