@geonosis/doctor 1.0.0 → 1.1.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 +99 -11
- package/bin/geonosis-doctor.mjs +24 -1
- package/dist/{chunk-R4AHDFE3.js → chunk-CG2LN4VW.js} +380 -125
- package/dist/doctor-cli.js +11 -3
- package/dist/index.d.ts +45 -12
- package/dist/index.js +7 -1
- package/package.json +7 -3
|
@@ -144,6 +144,164 @@ var checkBaseline = ({ ref, root }) => {
|
|
|
144
144
|
];
|
|
145
145
|
};
|
|
146
146
|
|
|
147
|
+
// src/deployed.ts
|
|
148
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
149
|
+
import { join as join2 } from "path";
|
|
150
|
+
var DEPLOYED_FILE = ".geonosis/deployed.json";
|
|
151
|
+
var NOT_WRITTEN = "no .geonosis/deployed.json \u2014 it is written by the pipeline after promote, and its absence is not a pass";
|
|
152
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
153
|
+
var listOf = (found) => Array.isArray(found) ? found.filter((one) => typeof one === "string") : [];
|
|
154
|
+
var finding2 = (verdict, subject, message) => ({
|
|
155
|
+
check: "deployed",
|
|
156
|
+
message,
|
|
157
|
+
subject,
|
|
158
|
+
verdict
|
|
159
|
+
});
|
|
160
|
+
var parseJsonc = (source) => {
|
|
161
|
+
let out = "";
|
|
162
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
163
|
+
const char = source[index] ?? "";
|
|
164
|
+
if (char === '"') {
|
|
165
|
+
const start = index;
|
|
166
|
+
index += 1;
|
|
167
|
+
for (; index < source.length; index += 1) {
|
|
168
|
+
if (source[index] === "\\") {
|
|
169
|
+
index += 1;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (source[index] === '"') break;
|
|
173
|
+
}
|
|
174
|
+
out += source.slice(start, index + 1);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (char === "/" && source[index + 1] === "/") {
|
|
178
|
+
const end = source.indexOf("\n", index);
|
|
179
|
+
index = end === -1 ? source.length : end - 1;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (char === "/" && source[index + 1] === "*") {
|
|
183
|
+
const end = source.indexOf("*/", index + 2);
|
|
184
|
+
index = end === -1 ? source.length : end + 1;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
out += char;
|
|
188
|
+
}
|
|
189
|
+
return JSON.parse(out.replaceAll(/,(\s*[\]}])/g, "$1"));
|
|
190
|
+
};
|
|
191
|
+
var BINDING_LISTS = [
|
|
192
|
+
"ai",
|
|
193
|
+
"analytics_engine_datasets",
|
|
194
|
+
"browser",
|
|
195
|
+
"d1_databases",
|
|
196
|
+
"dispatch_namespaces",
|
|
197
|
+
"durable_objects",
|
|
198
|
+
"hyperdrive",
|
|
199
|
+
"kv_namespaces",
|
|
200
|
+
"mtls_certificates",
|
|
201
|
+
"queues",
|
|
202
|
+
"r2_buckets",
|
|
203
|
+
"send_email",
|
|
204
|
+
"services",
|
|
205
|
+
"vectorize",
|
|
206
|
+
"version_metadata",
|
|
207
|
+
"workflows"
|
|
208
|
+
];
|
|
209
|
+
var bindingsOf = (found) => {
|
|
210
|
+
if (Array.isArray(found)) return found.flatMap(bindingsOf);
|
|
211
|
+
if (!isRecord(found)) return [];
|
|
212
|
+
const named2 = found["binding"] ?? found["name"];
|
|
213
|
+
return [
|
|
214
|
+
...typeof named2 === "string" ? [named2] : [],
|
|
215
|
+
...Object.entries(found).filter(([key]) => key === "bindings" || key === "producers" || key === "consumers").flatMap(([, one]) => bindingsOf(one))
|
|
216
|
+
];
|
|
217
|
+
};
|
|
218
|
+
var declaredIn = (config) => {
|
|
219
|
+
const triggers = config["triggers"];
|
|
220
|
+
const one = config["route"];
|
|
221
|
+
const listed = [
|
|
222
|
+
...Array.isArray(config["routes"]) ? config["routes"] : [],
|
|
223
|
+
...typeof one === "string" ? [one] : []
|
|
224
|
+
];
|
|
225
|
+
return {
|
|
226
|
+
bindings: BINDING_LISTS.flatMap((key) => bindingsOf(config[key])),
|
|
227
|
+
crons: isRecord(triggers) ? listOf(triggers["crons"]) : [],
|
|
228
|
+
routes: listed.map((route) => isRecord(route) ? route["pattern"] : route).filter((pattern) => typeof pattern === "string")
|
|
229
|
+
};
|
|
230
|
+
};
|
|
231
|
+
var differences = (kind, declared, deployed) => {
|
|
232
|
+
const missing = declared.filter((name) => !deployed.includes(name));
|
|
233
|
+
const extra = deployed.filter((name) => !declared.includes(name));
|
|
234
|
+
return [
|
|
235
|
+
...missing.length === 0 ? [] : [`declared but not deployed ${kind}: ${missing.join(", ")}`],
|
|
236
|
+
...extra.length === 0 ? [] : [`deployed but not declared ${kind}: ${extra.join(", ")}`]
|
|
237
|
+
];
|
|
238
|
+
};
|
|
239
|
+
var releaseOf = (root) => {
|
|
240
|
+
const path = join2(root, "geonosis.json");
|
|
241
|
+
if (!existsSync(path)) return void 0;
|
|
242
|
+
try {
|
|
243
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
244
|
+
return isRecord(parsed) && isRecord(parsed["release"]) ? parsed["release"] : {};
|
|
245
|
+
} catch {
|
|
246
|
+
return {};
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
var checkDeployed = ({ root }) => {
|
|
250
|
+
const release = releaseOf(root);
|
|
251
|
+
if (release === void 0) return [];
|
|
252
|
+
const configs = listOf(release["wrangler"]);
|
|
253
|
+
const secrets = listOf(release["secrets"]);
|
|
254
|
+
if (configs.length === 0 && secrets.length === 0) {
|
|
255
|
+
return [
|
|
256
|
+
finding2(
|
|
257
|
+
"SKIP",
|
|
258
|
+
"geonosis.json",
|
|
259
|
+
"release.wrangler and release.secrets are both empty \u2014 this repo has declared nothing a deployment is supposed to carry"
|
|
260
|
+
)
|
|
261
|
+
];
|
|
262
|
+
}
|
|
263
|
+
const env = typeof release["wranglerEnv"] === "string" ? release["wranglerEnv"] : void 0;
|
|
264
|
+
const declared = { bindings: [], crons: [], routes: [] };
|
|
265
|
+
for (const relative of configs) {
|
|
266
|
+
let parsed;
|
|
267
|
+
try {
|
|
268
|
+
parsed = parseJsonc(readFileSync2(join2(root, relative), "utf8"));
|
|
269
|
+
} catch (error) {
|
|
270
|
+
return [finding2("FAIL", relative, `could not be read: ${error.message}`)];
|
|
271
|
+
}
|
|
272
|
+
if (!isRecord(parsed)) return [finding2("FAIL", relative, "is not a wrangler configuration")];
|
|
273
|
+
const environments = parsed["env"];
|
|
274
|
+
const block = env !== void 0 && isRecord(environments) && isRecord(environments[env]) ? environments[env] : parsed;
|
|
275
|
+
const one = declaredIn(block);
|
|
276
|
+
declared.bindings.push(...one.bindings);
|
|
277
|
+
declared.crons.push(...one.crons);
|
|
278
|
+
declared.routes.push(...one.routes);
|
|
279
|
+
}
|
|
280
|
+
const path = join2(root, DEPLOYED_FILE);
|
|
281
|
+
if (!existsSync(path)) return [finding2("SKIP", DEPLOYED_FILE, NOT_WRITTEN)];
|
|
282
|
+
let reported;
|
|
283
|
+
try {
|
|
284
|
+
reported = JSON.parse(readFileSync2(path, "utf8"));
|
|
285
|
+
} catch (error) {
|
|
286
|
+
return [finding2("FAIL", DEPLOYED_FILE, `is not readable JSON: ${error.message}`)];
|
|
287
|
+
}
|
|
288
|
+
if (!isRecord(reported)) return [finding2("FAIL", DEPLOYED_FILE, "is not an object")];
|
|
289
|
+
const triggers = isRecord(reported["triggers"]) ? reported["triggers"] : {};
|
|
290
|
+
const drift = [
|
|
291
|
+
...differences("crons", declared.crons, listOf(triggers["crons"])),
|
|
292
|
+
...differences("routes", declared.routes, listOf(triggers["routes"])),
|
|
293
|
+
...differences("bindings", declared.bindings, listOf(reported["bindings"])),
|
|
294
|
+
...differences("secrets", secrets, listOf(reported["secrets"]))
|
|
295
|
+
];
|
|
296
|
+
return [
|
|
297
|
+
drift.length === 0 ? finding2(
|
|
298
|
+
"OK",
|
|
299
|
+
DEPLOYED_FILE,
|
|
300
|
+
`what the tree declares is what the pipeline reported${typeof reported["at"] === "string" ? ` at ${reported["at"]}` : ""}`
|
|
301
|
+
) : finding2("FAIL", DEPLOYED_FILE, drift.join("; "))
|
|
302
|
+
];
|
|
303
|
+
};
|
|
304
|
+
|
|
147
305
|
// src/types.ts
|
|
148
306
|
var CHECKS = [
|
|
149
307
|
"loaded",
|
|
@@ -151,7 +309,8 @@ var CHECKS = [
|
|
|
151
309
|
"baseline",
|
|
152
310
|
"runner",
|
|
153
311
|
"drift",
|
|
154
|
-
"observability"
|
|
312
|
+
"observability",
|
|
313
|
+
"deployed"
|
|
155
314
|
];
|
|
156
315
|
var DoctorError = class extends Error {
|
|
157
316
|
constructor(message) {
|
|
@@ -161,18 +320,18 @@ var DoctorError = class extends Error {
|
|
|
161
320
|
};
|
|
162
321
|
|
|
163
322
|
// src/resolve.ts
|
|
164
|
-
import { existsSync, readFileSync as
|
|
323
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3, realpathSync } from "fs";
|
|
165
324
|
import { createRequire } from "module";
|
|
166
|
-
import { dirname, join as
|
|
325
|
+
import { dirname, join as join3 } from "path";
|
|
167
326
|
import { pathToFileURL } from "url";
|
|
168
|
-
var resolveFrom = (dir, specifier) => createRequire(
|
|
327
|
+
var resolveFrom = (dir, specifier) => createRequire(join3(dir, "noop.js")).resolve(specifier);
|
|
169
328
|
var packageDirOf = (entry, name) => {
|
|
170
329
|
let dir = dirname(entry);
|
|
171
330
|
for (; ; ) {
|
|
172
|
-
const manifest =
|
|
173
|
-
if (
|
|
331
|
+
const manifest = join3(dir, "package.json");
|
|
332
|
+
if (existsSync2(manifest)) {
|
|
174
333
|
try {
|
|
175
|
-
const parsed = JSON.parse(
|
|
334
|
+
const parsed = JSON.parse(readFileSync3(manifest, "utf8"));
|
|
176
335
|
if (parsed.name === name) return dir;
|
|
177
336
|
} catch {
|
|
178
337
|
}
|
|
@@ -194,7 +353,16 @@ var pluginVersionOf = async (entry) => {
|
|
|
194
353
|
}
|
|
195
354
|
return version;
|
|
196
355
|
};
|
|
197
|
-
var
|
|
356
|
+
var probesOf = async (entry, plugin) => {
|
|
357
|
+
const loaded = await import(pathToFileURL(entry).href);
|
|
358
|
+
return Object.fromEntries(
|
|
359
|
+
Object.entries(loaded.default?.rules ?? {}).filter(([, rule]) => typeof rule?.probe === "function").map(([name, rule]) => [
|
|
360
|
+
`${plugin}/${name}`,
|
|
361
|
+
rule.probe
|
|
362
|
+
])
|
|
363
|
+
);
|
|
364
|
+
};
|
|
365
|
+
var corpusOfPlugin = (from, specifier) => join3(packageDirOf(resolveFrom(from, specifier), specifier), "corpus");
|
|
198
366
|
var real = (path) => {
|
|
199
367
|
try {
|
|
200
368
|
return realpathSync(path);
|
|
@@ -205,8 +373,8 @@ var real = (path) => {
|
|
|
205
373
|
var relativeToRoot = (root, path) => relativePath(real(root), real(path));
|
|
206
374
|
|
|
207
375
|
// src/drift.ts
|
|
208
|
-
import { existsSync as
|
|
209
|
-
import { join as
|
|
376
|
+
import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
|
|
377
|
+
import { join as join4, sep as sep2 } from "path";
|
|
210
378
|
var WORKFLOWS = ".github/workflows";
|
|
211
379
|
var SETTINGS = ".claude/settings.json";
|
|
212
380
|
var GEONOSIS = "geonosis.json";
|
|
@@ -214,7 +382,7 @@ var LAW = "CLAUDE.md";
|
|
|
214
382
|
var CEILING = 200;
|
|
215
383
|
var SWITCHED_OFF = /^\s*if:\s*(?:\$\{\{\s*)?false\b/m;
|
|
216
384
|
var TEST_FILE = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
217
|
-
var
|
|
385
|
+
var finding3 = (subject, verdict, message) => ({
|
|
218
386
|
check: "drift",
|
|
219
387
|
message,
|
|
220
388
|
subject,
|
|
@@ -232,27 +400,27 @@ var filesUnder = (dir, match) => {
|
|
|
232
400
|
}
|
|
233
401
|
for (const entry of entries) {
|
|
234
402
|
if (entry.isDirectory()) {
|
|
235
|
-
if (!entry.name.startsWith(".") && !NEVER_WALKED2.has(entry.name)) walk2(
|
|
403
|
+
if (!entry.name.startsWith(".") && !NEVER_WALKED2.has(entry.name)) walk2(join4(at, entry.name));
|
|
236
404
|
continue;
|
|
237
405
|
}
|
|
238
|
-
if (entry.isFile() && match(entry.name)) found.push(
|
|
406
|
+
if (entry.isFile() && match(entry.name)) found.push(join4(at, entry.name));
|
|
239
407
|
}
|
|
240
408
|
};
|
|
241
409
|
walk2(dir);
|
|
242
410
|
return found;
|
|
243
411
|
};
|
|
244
412
|
var ci = (root) => {
|
|
245
|
-
const dir =
|
|
246
|
-
if (!
|
|
247
|
-
return [
|
|
413
|
+
const dir = join4(root, WORKFLOWS);
|
|
414
|
+
if (!existsSync3(dir)) {
|
|
415
|
+
return [finding3(WORKFLOWS, "SKIP", "there are no workflows here to read")];
|
|
248
416
|
}
|
|
249
417
|
return filesUnder(dir, (name) => name.endsWith(".yml") || name.endsWith(".yaml")).map((path) => {
|
|
250
418
|
const at = relativePath(root, path);
|
|
251
|
-
return SWITCHED_OFF.test(
|
|
419
|
+
return SWITCHED_OFF.test(readFileSync4(path, "utf8")) ? finding3(
|
|
252
420
|
at,
|
|
253
421
|
"FAIL",
|
|
254
422
|
"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
|
-
) :
|
|
423
|
+
) : finding3(at, "OK", "nothing in it is switched off");
|
|
256
424
|
});
|
|
257
425
|
};
|
|
258
426
|
var holds = (parent, child) => child === parent || child.startsWith(`${parent}${sep2}`);
|
|
@@ -268,10 +436,10 @@ var orphanTests = (root, workspaces) => {
|
|
|
268
436
|
orphaned.set(at, [...orphaned.get(at) ?? [], relativePath(root, path)]);
|
|
269
437
|
}
|
|
270
438
|
if (orphaned.size === 0) {
|
|
271
|
-
return [
|
|
439
|
+
return [finding3("test files", "OK", "every test file sits under a workspace that runs tests")];
|
|
272
440
|
}
|
|
273
441
|
return [...orphaned.entries()].map(
|
|
274
|
-
([at, files]) =>
|
|
442
|
+
([at, files]) => finding3(
|
|
275
443
|
at,
|
|
276
444
|
"FAIL",
|
|
277
445
|
`${files.length} test file(s) here and no test script to run them \u2014 ${files.slice(0, 3).join(", ")}`
|
|
@@ -279,10 +447,10 @@ var orphanTests = (root, workspaces) => {
|
|
|
279
447
|
);
|
|
280
448
|
};
|
|
281
449
|
var readGeonosis = (root) => {
|
|
282
|
-
const path =
|
|
283
|
-
if (!
|
|
450
|
+
const path = join4(root, GEONOSIS);
|
|
451
|
+
if (!existsSync3(path)) return void 0;
|
|
284
452
|
try {
|
|
285
|
-
return JSON.parse(
|
|
453
|
+
return JSON.parse(readFileSync4(path, "utf8"));
|
|
286
454
|
} catch {
|
|
287
455
|
return void 0;
|
|
288
456
|
}
|
|
@@ -291,34 +459,34 @@ var law = (root, config) => {
|
|
|
291
459
|
const declared = config?.law ?? {};
|
|
292
460
|
const file = typeof declared.file === "string" ? declared.file : LAW;
|
|
293
461
|
const ceiling = typeof declared.maxLines === "number" ? declared.maxLines : CEILING;
|
|
294
|
-
const path =
|
|
295
|
-
if (!
|
|
296
|
-
return [
|
|
462
|
+
const path = join4(root, file);
|
|
463
|
+
if (!existsSync3(path)) {
|
|
464
|
+
return [finding3(file, "SKIP", "there is no law file here to measure")];
|
|
297
465
|
}
|
|
298
|
-
const source =
|
|
466
|
+
const source = readFileSync4(path, "utf8");
|
|
299
467
|
const lines = source.split("\n").length - (source.endsWith("\n") ? 1 : 0);
|
|
300
468
|
return [
|
|
301
|
-
lines > ceiling ?
|
|
469
|
+
lines > ceiling ? finding3(
|
|
302
470
|
file,
|
|
303
471
|
"WARN",
|
|
304
472
|
`${lines} lines against a ceiling of ${ceiling} \u2014 depth belongs in the skills and in the rules, where it is read`
|
|
305
|
-
) :
|
|
473
|
+
) : finding3(file, "OK", `${lines} lines, under the ceiling of ${ceiling}`)
|
|
306
474
|
];
|
|
307
475
|
};
|
|
308
476
|
var hooks = (root) => {
|
|
309
|
-
const path =
|
|
310
|
-
if (!
|
|
477
|
+
const path = join4(root, SETTINGS);
|
|
478
|
+
if (!existsSync3(path)) {
|
|
311
479
|
return [
|
|
312
|
-
|
|
480
|
+
finding3(
|
|
313
481
|
SETTINGS,
|
|
314
482
|
"WARN",
|
|
315
483
|
"nothing here installs the kit\u2019s plugin, so none of the hooks, agents or skills reach this repo \u2014 the gates run, the method does not"
|
|
316
484
|
)
|
|
317
485
|
];
|
|
318
486
|
}
|
|
319
|
-
const source =
|
|
487
|
+
const source = readFileSync4(path, "utf8");
|
|
320
488
|
return [
|
|
321
|
-
source.includes("geonosis") ?
|
|
489
|
+
source.includes("geonosis") ? finding3(SETTINGS, "OK", "it installs the kit\u2019s plugin") : finding3(SETTINGS, "WARN", "it names no geonosis plugin or marketplace")
|
|
322
490
|
];
|
|
323
491
|
};
|
|
324
492
|
var READERS = {
|
|
@@ -339,7 +507,7 @@ var resolves = (root, name) => {
|
|
|
339
507
|
var blocks = (root, config, readers) => {
|
|
340
508
|
if (config === void 0) {
|
|
341
509
|
return [
|
|
342
|
-
|
|
510
|
+
finding3(
|
|
343
511
|
GEONOSIS,
|
|
344
512
|
"SKIP",
|
|
345
513
|
"there is no geonosis.json here, so no block says what this repo asks the kit to do"
|
|
@@ -351,7 +519,7 @@ var blocks = (root, config, readers) => {
|
|
|
351
519
|
const installed = resolves(root, name);
|
|
352
520
|
if (declared && !installed) {
|
|
353
521
|
return [
|
|
354
|
-
|
|
522
|
+
finding3(
|
|
355
523
|
name,
|
|
356
524
|
"WARN",
|
|
357
525
|
`geonosis.json has a "${block}" block and ${name} is not installed here \u2014 nothing reads it`
|
|
@@ -360,21 +528,21 @@ var blocks = (root, config, readers) => {
|
|
|
360
528
|
}
|
|
361
529
|
if (!declared && installed) {
|
|
362
530
|
return [
|
|
363
|
-
|
|
531
|
+
finding3(
|
|
364
532
|
name,
|
|
365
533
|
"WARN",
|
|
366
534
|
`${name} is installed and geonosis.json has no "${block}" block \u2014 it runs on its defaults, whatever they are`
|
|
367
535
|
)
|
|
368
536
|
];
|
|
369
537
|
}
|
|
370
|
-
return declared ? [
|
|
538
|
+
return declared ? [finding3(name, "OK", `a "${block}" block, and ${name} to read it`)] : [];
|
|
371
539
|
});
|
|
372
540
|
};
|
|
373
541
|
var pluginDirsConfigured = (root) => {
|
|
374
|
-
const path =
|
|
375
|
-
if (!
|
|
542
|
+
const path = join4(root, ".oxlintrc.json");
|
|
543
|
+
if (!existsSync3(path)) return void 0;
|
|
376
544
|
try {
|
|
377
|
-
const parsed = JSON.parse(
|
|
545
|
+
const parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
378
546
|
const rule = parsed.rules?.["biological-architecture/no-unregistered-plugin-dir"];
|
|
379
547
|
const options = Array.isArray(rule) ? rule[1] : void 0;
|
|
380
548
|
if (options?.roots === void 0 || typeof options.registry !== "string") return void 0;
|
|
@@ -391,26 +559,26 @@ var pluginDirs = (root) => {
|
|
|
391
559
|
const configured = pluginDirsConfigured(root);
|
|
392
560
|
if (configured === void 0) {
|
|
393
561
|
return [
|
|
394
|
-
|
|
562
|
+
finding3(
|
|
395
563
|
"plugin directories",
|
|
396
564
|
"SKIP",
|
|
397
565
|
"no-unregistered-plugin-dir is not configured here, so this repo has not said where its integrations live"
|
|
398
566
|
)
|
|
399
567
|
];
|
|
400
568
|
}
|
|
401
|
-
const registryPath =
|
|
402
|
-
if (!
|
|
403
|
-
return [
|
|
569
|
+
const registryPath = join4(root, configured.registry);
|
|
570
|
+
if (!existsSync3(registryPath)) {
|
|
571
|
+
return [finding3(configured.registry, "FAIL", "the registry the rule names is not there")];
|
|
404
572
|
}
|
|
405
|
-
const registry =
|
|
573
|
+
const registry = readFileSync4(registryPath, "utf8");
|
|
406
574
|
const unreachable = [];
|
|
407
575
|
for (const rootDir of configured.roots) {
|
|
408
|
-
const at =
|
|
409
|
-
if (!
|
|
576
|
+
const at = join4(root, rootDir);
|
|
577
|
+
if (!existsSync3(at)) continue;
|
|
410
578
|
for (const entry of readdirSync2(at, { withFileTypes: true })) {
|
|
411
579
|
if (!entry.isDirectory()) continue;
|
|
412
580
|
const hasManifest = configured.manifests.some(
|
|
413
|
-
(name) =>
|
|
581
|
+
(name) => existsSync3(join4(at, entry.name, name))
|
|
414
582
|
);
|
|
415
583
|
if (hasManifest && !registry.includes(entry.name)) {
|
|
416
584
|
unreachable.push(`${rootDir}/${entry.name}`);
|
|
@@ -418,11 +586,11 @@ var pluginDirs = (root) => {
|
|
|
418
586
|
}
|
|
419
587
|
}
|
|
420
588
|
return [
|
|
421
|
-
unreachable.length === 0 ?
|
|
589
|
+
unreachable.length === 0 ? finding3(
|
|
422
590
|
configured.registry,
|
|
423
591
|
"OK",
|
|
424
592
|
"every directory under the declared roots is named by it"
|
|
425
|
-
) :
|
|
593
|
+
) : finding3(
|
|
426
594
|
configured.registry,
|
|
427
595
|
"FAIL",
|
|
428
596
|
`${unreachable.length} directory(ies) it never names: ${unreachable.join(", ")}`
|
|
@@ -447,14 +615,15 @@ var checkDrift = ({
|
|
|
447
615
|
|
|
448
616
|
// src/exercised.ts
|
|
449
617
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
450
|
-
import { existsSync as
|
|
618
|
+
import { cpSync, existsSync as existsSync4, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
451
619
|
import { tmpdir } from "os";
|
|
452
|
-
import { join as
|
|
620
|
+
import { dirname as dirname2, join as join5 } from "path";
|
|
453
621
|
import { corpusOf, readManifest } from "@geonosis/lint-parity";
|
|
454
622
|
var OFF = /* @__PURE__ */ new Set([0, "0", "allow", "off", false]);
|
|
455
623
|
var severityOf = (level) => Array.isArray(level) ? level[0] : level;
|
|
456
624
|
var enabledRulesOf = (rules, plugin) => Object.keys(rules).filter((id) => id.startsWith(`${plugin}/`) && !OFF.has(severityOf(rules[id]))).toSorted();
|
|
457
|
-
var
|
|
625
|
+
var passes = (findings) => !findings.some((one) => one.verdict === "FAIL" || one.verdict === "UNJUDGED");
|
|
626
|
+
var finding4 = (subject, verdict, message) => ({
|
|
458
627
|
check: "exercised",
|
|
459
628
|
message,
|
|
460
629
|
subject,
|
|
@@ -466,9 +635,9 @@ var refusal = (error) => {
|
|
|
466
635
|
return said.length > LIMIT ? `${said.slice(0, LIMIT)}\u2026` : said;
|
|
467
636
|
};
|
|
468
637
|
var reasonFrom = (config, oxlint) => {
|
|
469
|
-
const dir = mkdtempSync(
|
|
638
|
+
const dir = mkdtempSync(join5(tmpdir(), "geonosis-doctor-why-"));
|
|
470
639
|
try {
|
|
471
|
-
const probe =
|
|
640
|
+
const probe = join5(dir, "probe.tsx");
|
|
472
641
|
writeFileSync(probe, "export const probe = 1\n");
|
|
473
642
|
const run = spawnSync2(
|
|
474
643
|
oxlint,
|
|
@@ -502,21 +671,59 @@ var ownReach = ({
|
|
|
502
671
|
fired: new Set(reach.filter((one) => one.firedInA).map((one) => one.rule))
|
|
503
672
|
};
|
|
504
673
|
};
|
|
505
|
-
var
|
|
674
|
+
var optionsOf = (level) => Array.isArray(level) ? level.slice(1) : [];
|
|
675
|
+
var under = (config, rule) => `${rule} under ${JSON.stringify(optionsOf(config.rules[rule]))}`;
|
|
676
|
+
var throughProbes = ({
|
|
506
677
|
config,
|
|
507
678
|
corpus,
|
|
508
679
|
oxlint,
|
|
680
|
+
probes,
|
|
681
|
+
silent
|
|
682
|
+
}) => {
|
|
683
|
+
const refused = /* @__PURE__ */ new Map();
|
|
684
|
+
const dir = mkdtempSync(join5(tmpdir(), "geonosis-doctor-probe-"));
|
|
685
|
+
const here = join5(dir, "corpus");
|
|
686
|
+
try {
|
|
687
|
+
cpSync(corpus, here, { recursive: true });
|
|
688
|
+
for (const rule of silent) {
|
|
689
|
+
const write = probes[rule];
|
|
690
|
+
if (write === void 0) continue;
|
|
691
|
+
try {
|
|
692
|
+
const probe = write(optionsOf(config.rules[rule]));
|
|
693
|
+
const at = join5(here, probe.path);
|
|
694
|
+
mkdirSync(dirname2(at), { recursive: true });
|
|
695
|
+
writeFileSync(at, probe.source);
|
|
696
|
+
} catch (error) {
|
|
697
|
+
refused.set(rule, String(error.message));
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
const reach = corpusOf({
|
|
701
|
+
configA: config.path,
|
|
702
|
+
configB: config.path,
|
|
703
|
+
corpus: here,
|
|
704
|
+
oxlint
|
|
705
|
+
}).reach;
|
|
706
|
+
return { fired: new Set(reach.filter((one) => one.firedInA).map((one) => one.rule)), refused };
|
|
707
|
+
} finally {
|
|
708
|
+
rmSync(dir, { force: true, recursive: true });
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
var checkExercised = async ({
|
|
712
|
+
config,
|
|
713
|
+
corpus,
|
|
714
|
+
entry,
|
|
715
|
+
oxlint,
|
|
509
716
|
repoCorpus,
|
|
510
717
|
root
|
|
511
718
|
}) => {
|
|
512
|
-
const said = (verdict, message) =>
|
|
513
|
-
if (repoCorpus !== void 0 && !
|
|
719
|
+
const said = (verdict, message) => finding4(config.relative, verdict, message);
|
|
720
|
+
if (repoCorpus !== void 0 && !existsSync4(repoCorpus)) {
|
|
514
721
|
return said(
|
|
515
722
|
"FAIL",
|
|
516
723
|
`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
724
|
);
|
|
518
725
|
}
|
|
519
|
-
if (!
|
|
726
|
+
if (!existsSync4(corpus)) {
|
|
520
727
|
return said(
|
|
521
728
|
"SKIP",
|
|
522
729
|
`the plugin loaded from here ships no corpus at ${relativeToRoot(root, corpus)} \u2014 nothing declares which rules it can be evidence about`
|
|
@@ -564,7 +771,38 @@ var checkExercised = ({
|
|
|
564
771
|
}
|
|
565
772
|
const silent = enabled.filter((rule) => !fired.has(rule) && !own.fired.has(rule));
|
|
566
773
|
const where = own.claimed.length === 0 ? "" : ` (${own.fired.size} by this repo's own corpus)`;
|
|
567
|
-
|
|
774
|
+
if (silent.length === 0) {
|
|
775
|
+
return said("OK", `${enabled.length} enabled, ${enabled.length} exercised${where}`);
|
|
776
|
+
}
|
|
777
|
+
const probes = entry === void 0 ? {} : await probesOf(entry, manifest.plugin).catch(() => ({}));
|
|
778
|
+
const declared = silent.filter((rule) => probes[rule] !== void 0);
|
|
779
|
+
const unprobed = silent.filter((rule) => probes[rule] === void 0);
|
|
780
|
+
let probed = { fired: /* @__PURE__ */ new Set(), refused: /* @__PURE__ */ new Map() };
|
|
781
|
+
if (declared.length > 0) {
|
|
782
|
+
try {
|
|
783
|
+
probed = throughProbes({ config, corpus, oxlint, probes, silent: declared });
|
|
784
|
+
} catch (error) {
|
|
785
|
+
return said("FAIL", `the declared probes could not be run \u2014 ${refusal(error)}`);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
const exercised = declared.filter((rule) => probed.fired.has(rule));
|
|
789
|
+
const inert = declared.filter((rule) => !probed.fired.has(rule));
|
|
790
|
+
const placed = inert.filter((rule) => !probed.refused.has(rule));
|
|
791
|
+
if (placed.length > 0) {
|
|
792
|
+
return said(
|
|
793
|
+
"FAIL",
|
|
794
|
+
`${countOf(placed, "fire")} nowhere in the corpus and nothing through the probe each declares either: ${named(placed.map((rule) => under(config, rule)))}`
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
const unplaceable = [
|
|
798
|
+
...unprobed.map(
|
|
799
|
+
(rule) => `${rule} declares no probe, so ${optionsOf(config.rules[rule]).length === 0 ? "its scope" : JSON.stringify(optionsOf(config.rules[rule]))} does not reach the corpus`
|
|
800
|
+
),
|
|
801
|
+
...inert.map((rule) => `${rule}: ${probed.refused.get(rule) ?? ""}`)
|
|
802
|
+
];
|
|
803
|
+
if (unplaceable.length > 0) return said("UNJUDGED", named(unplaceable));
|
|
804
|
+
const through = exercised.length === 1 ? "1 through its declared probe under this repo\u2019s options" : `${exercised.length} through their declared probes under this repo\u2019s options`;
|
|
805
|
+
return said("OK", `${enabled.length} enabled, ${enabled.length} exercised${where} \u2014 ${through}`);
|
|
568
806
|
};
|
|
569
807
|
|
|
570
808
|
// src/loaded.ts
|
|
@@ -638,7 +876,7 @@ var declaredFor = ({
|
|
|
638
876
|
}
|
|
639
877
|
return void 0;
|
|
640
878
|
};
|
|
641
|
-
var
|
|
879
|
+
var finding5 = (subject, verdict, message) => ({
|
|
642
880
|
check: "loaded",
|
|
643
881
|
message,
|
|
644
882
|
subject,
|
|
@@ -655,7 +893,7 @@ var oneConfig = async ({
|
|
|
655
893
|
specifier,
|
|
656
894
|
workspaces
|
|
657
895
|
}) => {
|
|
658
|
-
const said = (verdict, message) =>
|
|
896
|
+
const said = (verdict, message) => finding5(config.relative, verdict, `${specifier}: ${message}`);
|
|
659
897
|
let loaded;
|
|
660
898
|
try {
|
|
661
899
|
loaded = await versionAt(resolveFrom(config.dir, specifier), specifier, root);
|
|
@@ -707,10 +945,10 @@ var copiesOf = async ({
|
|
|
707
945
|
found.set(at, { from: [labelOf(workspace)], version: await pluginVersionOf(entry) });
|
|
708
946
|
}
|
|
709
947
|
if (found.size === 0) {
|
|
710
|
-
return
|
|
948
|
+
return finding5(specifier, "FAIL", "no workspace in this tree can resolve it at all");
|
|
711
949
|
}
|
|
712
950
|
const listed = [...found.entries()].map(([at, one]) => `${at} ${one.version} (${one.from.join(", ")})`).join("; ");
|
|
713
|
-
return found.size === 1 ?
|
|
951
|
+
return found.size === 1 ? finding5(specifier, "OK", `1 copy \u2014 ${listed}`) : finding5(
|
|
714
952
|
specifier,
|
|
715
953
|
"WARN",
|
|
716
954
|
`${found.size} copies \u2014 ${listed}. Which one oxlint runs depends on which directory its config sits in.`
|
|
@@ -725,7 +963,7 @@ var checkLoaded = async ({
|
|
|
725
963
|
const specifiers = /* @__PURE__ */ new Set();
|
|
726
964
|
for (const config of configs) {
|
|
727
965
|
if (config.error !== void 0) {
|
|
728
|
-
findings.push(
|
|
966
|
+
findings.push(finding5(config.relative, "FAIL", config.error));
|
|
729
967
|
continue;
|
|
730
968
|
}
|
|
731
969
|
for (const specifier of config.jsPlugins.filter((name) => name.startsWith(SCOPE))) {
|
|
@@ -740,13 +978,13 @@ var checkLoaded = async ({
|
|
|
740
978
|
};
|
|
741
979
|
|
|
742
980
|
// src/observability.ts
|
|
743
|
-
import { readFileSync as
|
|
744
|
-
import { join as
|
|
981
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
982
|
+
import { join as join6 } from "path";
|
|
745
983
|
var GEONOSIS_FILE = "geonosis.json";
|
|
746
984
|
var REACHES_NOTHING = /* @__PURE__ */ new Set(["console", "memory", "noop", "none", "null", "swallowing"]);
|
|
747
985
|
var DEFAULT_MAX_AGE_SECONDS = 3600;
|
|
748
986
|
var HEAD_TIMEOUT_MS = 3e3;
|
|
749
|
-
var
|
|
987
|
+
var finding6 = (verdict, subject, message) => ({
|
|
750
988
|
check: "observability",
|
|
751
989
|
message,
|
|
752
990
|
subject,
|
|
@@ -755,7 +993,7 @@ var finding5 = (verdict, subject, message) => ({
|
|
|
755
993
|
var readGeonosis2 = (root) => {
|
|
756
994
|
let text;
|
|
757
995
|
try {
|
|
758
|
-
text =
|
|
996
|
+
text = readFileSync5(join6(root, GEONOSIS_FILE), "utf8");
|
|
759
997
|
} catch {
|
|
760
998
|
return { present: false };
|
|
761
999
|
}
|
|
@@ -773,25 +1011,25 @@ var readGeonosis2 = (root) => {
|
|
|
773
1011
|
var exporterFinding = (config) => {
|
|
774
1012
|
const sink = config.sink;
|
|
775
1013
|
if (typeof sink !== "string" || sink.trim() === "") {
|
|
776
|
-
return
|
|
1014
|
+
return finding6(
|
|
777
1015
|
"FAIL",
|
|
778
1016
|
GEONOSIS_FILE,
|
|
779
1017
|
"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
1018
|
);
|
|
781
1019
|
}
|
|
782
1020
|
if (REACHES_NOTHING.has(sink.toLowerCase())) {
|
|
783
|
-
return
|
|
1021
|
+
return finding6(
|
|
784
1022
|
"WARN",
|
|
785
1023
|
GEONOSIS_FILE,
|
|
786
1024
|
`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
1025
|
);
|
|
788
1026
|
}
|
|
789
|
-
return
|
|
1027
|
+
return finding6("OK", GEONOSIS_FILE, `the configured sink is "${sink}"`);
|
|
790
1028
|
};
|
|
791
1029
|
var reachableFinding = async (config) => {
|
|
792
1030
|
const endpoint = config.endpoint;
|
|
793
1031
|
if (typeof endpoint !== "string" || endpoint.trim() === "") {
|
|
794
|
-
return
|
|
1032
|
+
return finding6(
|
|
795
1033
|
"SKIP",
|
|
796
1034
|
GEONOSIS_FILE,
|
|
797
1035
|
"no observability.endpoint was named, so whether the exporter is reachable was not asked"
|
|
@@ -801,13 +1039,13 @@ var reachableFinding = async (config) => {
|
|
|
801
1039
|
const timer = setTimeout(() => controller.abort(), HEAD_TIMEOUT_MS);
|
|
802
1040
|
try {
|
|
803
1041
|
const response = await fetch(endpoint, { method: "HEAD", signal: controller.signal });
|
|
804
|
-
return
|
|
1042
|
+
return finding6(
|
|
805
1043
|
"OK",
|
|
806
1044
|
GEONOSIS_FILE,
|
|
807
1045
|
`${endpoint} is reachable \u2014 it answered ${response.status} to a HEAD`
|
|
808
1046
|
);
|
|
809
1047
|
} catch (error) {
|
|
810
|
-
return
|
|
1048
|
+
return finding6(
|
|
811
1049
|
"FAIL",
|
|
812
1050
|
GEONOSIS_FILE,
|
|
813
1051
|
`${endpoint} is not reachable from here: ${error.message}. Every report this repo sends is going into that.`
|
|
@@ -819,7 +1057,7 @@ var reachableFinding = async (config) => {
|
|
|
819
1057
|
var ageFinding = (config, root, now) => {
|
|
820
1058
|
const file = config.lastEventFile;
|
|
821
1059
|
if (typeof file !== "string" || file.trim() === "") {
|
|
822
|
-
return
|
|
1060
|
+
return finding6(
|
|
823
1061
|
"SKIP",
|
|
824
1062
|
GEONOSIS_FILE,
|
|
825
1063
|
"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."
|
|
@@ -828,27 +1066,27 @@ var ageFinding = (config, root, now) => {
|
|
|
828
1066
|
const maxAgeSeconds = typeof config.maxAgeSeconds === "number" && config.maxAgeSeconds > 0 ? config.maxAgeSeconds : DEFAULT_MAX_AGE_SECONDS;
|
|
829
1067
|
let record;
|
|
830
1068
|
try {
|
|
831
|
-
record = JSON.parse(
|
|
1069
|
+
record = JSON.parse(readFileSync5(join6(root, file), "utf8"));
|
|
832
1070
|
} catch (error) {
|
|
833
|
-
return
|
|
1071
|
+
return finding6(
|
|
834
1072
|
"FAIL",
|
|
835
1073
|
file,
|
|
836
1074
|
`the last event file could not be read: ${error.message}. A sink that has never written one has never captured anything.`
|
|
837
1075
|
);
|
|
838
1076
|
}
|
|
839
1077
|
if (typeof record.at !== "number" || !Number.isFinite(record.at)) {
|
|
840
|
-
return
|
|
1078
|
+
return finding6(
|
|
841
1079
|
"FAIL",
|
|
842
1080
|
file,
|
|
843
1081
|
'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
1082
|
);
|
|
845
1083
|
}
|
|
846
1084
|
const ageSeconds = Math.round((now - record.at) / 1e3);
|
|
847
|
-
return ageSeconds > maxAgeSeconds ?
|
|
1085
|
+
return ageSeconds > maxAgeSeconds ? finding6(
|
|
848
1086
|
"FAIL",
|
|
849
1087
|
file,
|
|
850
1088
|
`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
|
-
) :
|
|
1089
|
+
) : finding6(
|
|
852
1090
|
"OK",
|
|
853
1091
|
file,
|
|
854
1092
|
`the last event arrived ${ageSeconds}s ago, inside the ${maxAgeSeconds}s window`
|
|
@@ -857,16 +1095,16 @@ var ageFinding = (config, root, now) => {
|
|
|
857
1095
|
var probeFinding = (config) => {
|
|
858
1096
|
const probe = config.probe;
|
|
859
1097
|
if (typeof probe === "string" && probe.trim() !== "") {
|
|
860
|
-
return
|
|
1098
|
+
return finding6("OK", GEONOSIS_FILE, `the probe that proves this exporter is "${probe}"`);
|
|
861
1099
|
}
|
|
862
1100
|
if (typeof config.lastEventFile === "string" && config.lastEventFile.trim() !== "") {
|
|
863
|
-
return
|
|
1101
|
+
return finding6(
|
|
864
1102
|
"OK",
|
|
865
1103
|
GEONOSIS_FILE,
|
|
866
1104
|
"no probe command, but a last event file is read above, so something does look at this exporter"
|
|
867
1105
|
);
|
|
868
1106
|
}
|
|
869
|
-
return
|
|
1107
|
+
return finding6(
|
|
870
1108
|
"WARN",
|
|
871
1109
|
GEONOSIS_FILE,
|
|
872
1110
|
"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."
|
|
@@ -879,7 +1117,7 @@ var checkObservability = async ({
|
|
|
879
1117
|
const read = readGeonosis2(root);
|
|
880
1118
|
if (read.error !== void 0) {
|
|
881
1119
|
return [
|
|
882
|
-
|
|
1120
|
+
finding6(
|
|
883
1121
|
"FAIL",
|
|
884
1122
|
GEONOSIS_FILE,
|
|
885
1123
|
`${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.`
|
|
@@ -888,7 +1126,7 @@ var checkObservability = async ({
|
|
|
888
1126
|
}
|
|
889
1127
|
if (!read.present || read.config === void 0) {
|
|
890
1128
|
return [
|
|
891
|
-
|
|
1129
|
+
finding6(
|
|
892
1130
|
"SKIP",
|
|
893
1131
|
GEONOSIS_FILE,
|
|
894
1132
|
`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.`
|
|
@@ -905,16 +1143,16 @@ var checkObservability = async ({
|
|
|
905
1143
|
};
|
|
906
1144
|
|
|
907
1145
|
// src/repo-corpus.ts
|
|
908
|
-
import { existsSync as
|
|
909
|
-
import { join as
|
|
1146
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
|
|
1147
|
+
import { join as join7 } from "path";
|
|
910
1148
|
var GEONOSIS_FILE2 = "geonosis.json";
|
|
911
1149
|
var repoCorpusOf = (root) => {
|
|
912
|
-
const path =
|
|
913
|
-
if (!
|
|
1150
|
+
const path = join7(root, GEONOSIS_FILE2);
|
|
1151
|
+
if (!existsSync5(path)) return void 0;
|
|
914
1152
|
try {
|
|
915
|
-
const parsed = JSON.parse(
|
|
1153
|
+
const parsed = JSON.parse(readFileSync6(path, "utf8"));
|
|
916
1154
|
const declared = parsed.doctor?.corpus;
|
|
917
|
-
return typeof declared === "string" && declared !== "" ?
|
|
1155
|
+
return typeof declared === "string" && declared !== "" ? join7(root, declared) : void 0;
|
|
918
1156
|
} catch {
|
|
919
1157
|
return void 0;
|
|
920
1158
|
}
|
|
@@ -923,8 +1161,9 @@ var repoCorpusOf = (root) => {
|
|
|
923
1161
|
// src/runner.ts
|
|
924
1162
|
var TEST_FAILURES = "testFailures";
|
|
925
1163
|
var RUNS_A_RUNNER = /(?:^|[\s;&|(])(?:npx\s+|bunx\s+|pnpm\s+(?:exec\s+)?)?(?:vitest|bun\s+test)(?:\s|$)/;
|
|
1164
|
+
var RUNS_BUN_TEST = /(?:^|[\s;&|(])(?:bunx\s+)?bun\s+test(?:\s|$)/;
|
|
926
1165
|
var WRITES_A_REPORT = /--reporter[= ]\S*json|--outputFile/i;
|
|
927
|
-
var
|
|
1166
|
+
var finding7 = (subject, verdict, message) => ({
|
|
928
1167
|
check: "runner",
|
|
929
1168
|
message,
|
|
930
1169
|
subject,
|
|
@@ -952,7 +1191,7 @@ var checkRunner = ({
|
|
|
952
1191
|
if (script === "") return [];
|
|
953
1192
|
const subject = workspace.relative === "" ? "package.json" : `${workspace.relative}/package.json`;
|
|
954
1193
|
const said = (verdict, message) => [
|
|
955
|
-
|
|
1194
|
+
finding7(subject, verdict, message)
|
|
956
1195
|
];
|
|
957
1196
|
if (!RUNS_A_RUNNER.test(script)) {
|
|
958
1197
|
return said(
|
|
@@ -967,9 +1206,15 @@ var checkRunner = ({
|
|
|
967
1206
|
if (counter !== void 0) {
|
|
968
1207
|
return said("OK", `read by the ratchet's "${keyOf(counter)}" counter in report mode`);
|
|
969
1208
|
}
|
|
1209
|
+
if (RUNS_BUN_TEST.test(script)) {
|
|
1210
|
+
return said(
|
|
1211
|
+
"OK",
|
|
1212
|
+
`"${script}" is judged by its exit code, and bun test's exit code is a verdict: a planted failure exits 1, measured on bun 1.4.0 (2026-08-30). Nothing further is required here.`
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
970
1215
|
return said(
|
|
971
1216
|
"WARN",
|
|
972
|
-
`"${script}"
|
|
1217
|
+
`"${script}" runs vitest, whose exit code is the only verdict here; @cloudflare/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
1218
|
);
|
|
974
1219
|
});
|
|
975
1220
|
};
|
|
@@ -981,28 +1226,33 @@ var exercisedOf = ({
|
|
|
981
1226
|
oxlint,
|
|
982
1227
|
repoCorpus,
|
|
983
1228
|
root
|
|
984
|
-
}) =>
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1229
|
+
}) => Promise.all(
|
|
1230
|
+
configs.flatMap(
|
|
1231
|
+
(config) => config.jsPlugins.filter((name) => name.startsWith(SCOPE)).map(async (specifier) => {
|
|
1232
|
+
let corpus;
|
|
1233
|
+
let entry;
|
|
1234
|
+
try {
|
|
1235
|
+
corpus = corpusOfPlugin(config.dir, specifier);
|
|
1236
|
+
entry = resolveFrom(config.dir, specifier);
|
|
1237
|
+
} catch (error) {
|
|
1238
|
+
return {
|
|
1239
|
+
check: "exercised",
|
|
1240
|
+
message: `${specifier}: ${String(error.message)}`,
|
|
1241
|
+
subject: config.relative,
|
|
1242
|
+
verdict: "SKIP"
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
const own = repoCorpus !== void 0 && config.relative === CONFIG_FILE ? repoCorpus : void 0;
|
|
1246
|
+
return checkExercised({
|
|
1247
|
+
config,
|
|
1248
|
+
corpus,
|
|
1249
|
+
entry,
|
|
1250
|
+
oxlint,
|
|
1251
|
+
...own === void 0 ? {} : { repoCorpus: own },
|
|
1252
|
+
root
|
|
1253
|
+
});
|
|
1254
|
+
})
|
|
1255
|
+
)
|
|
1006
1256
|
);
|
|
1007
1257
|
var skip = (message) => [
|
|
1008
1258
|
{ check: "baseline", message, subject: RATCHET_FILE, verdict: "SKIP" }
|
|
@@ -1020,7 +1270,7 @@ var baselineOf = ({
|
|
|
1020
1270
|
return ref === void 0 ? skip("no ref was named and this repo has no origin/main to fall back on") : checkBaseline({ ref, root });
|
|
1021
1271
|
};
|
|
1022
1272
|
var ordered = (findings) => CHECKS.flatMap((check) => findings.filter((one) => one.check === check));
|
|
1023
|
-
var EMPTY = { FAIL: 0, OK: 0, SKIP: 0, WARN: 0 };
|
|
1273
|
+
var EMPTY = { FAIL: 0, OK: 0, SKIP: 0, UNJUDGED: 0, WARN: 0 };
|
|
1024
1274
|
var countsOf = (findings) => findings.reduce((counts, one) => ({ ...counts, [one.verdict]: counts[one.verdict] + 1 }), {
|
|
1025
1275
|
...EMPTY
|
|
1026
1276
|
});
|
|
@@ -1036,7 +1286,7 @@ var runDoctor = async ({
|
|
|
1036
1286
|
const repoCorpus = repoCorpusOf(root);
|
|
1037
1287
|
const found = ordered([
|
|
1038
1288
|
...await checkLoaded({ configs, root, workspaces }),
|
|
1039
|
-
...exercisedOf({
|
|
1289
|
+
...await exercisedOf({
|
|
1040
1290
|
configs,
|
|
1041
1291
|
oxlint: binary,
|
|
1042
1292
|
...repoCorpus === void 0 ? {} : { repoCorpus },
|
|
@@ -1045,13 +1295,14 @@ var runDoctor = async ({
|
|
|
1045
1295
|
...baselineOf({ baseline, root }),
|
|
1046
1296
|
...checkRunner({ ratchet: readRatchet(root), workspaces }),
|
|
1047
1297
|
...await checkObservability({ now: Date.now(), root }),
|
|
1048
|
-
...checkDrift({ root, workspaces })
|
|
1298
|
+
...checkDrift({ root, workspaces }),
|
|
1299
|
+
...checkDeployed({ root })
|
|
1049
1300
|
]);
|
|
1050
1301
|
const findings = strict ? found.map((one) => one.verdict === "WARN" ? { ...one, verdict: "FAIL" } : one) : found;
|
|
1051
1302
|
return {
|
|
1052
1303
|
counts: countsOf(findings),
|
|
1053
1304
|
findings,
|
|
1054
|
-
ok:
|
|
1305
|
+
ok: passes(findings),
|
|
1055
1306
|
root
|
|
1056
1307
|
};
|
|
1057
1308
|
};
|
|
@@ -1059,13 +1310,14 @@ var runDoctor = async ({
|
|
|
1059
1310
|
// src/report.ts
|
|
1060
1311
|
var ABOUT = {
|
|
1061
1312
|
baseline: "a number that may only shrink, against another ref",
|
|
1313
|
+
deployed: "what the pipeline reported deploying is what the tree declares",
|
|
1062
1314
|
drift: "the gates that were set up and are no longer running",
|
|
1063
1315
|
exercised: "every enabled rule fires on at least one corpus file",
|
|
1064
1316
|
loaded: "the plugin oxlint would load is the one the manifest pins",
|
|
1065
1317
|
observability: "an exporter is configured, reachable, and something arrived through it lately",
|
|
1066
1318
|
runner: "something reads the test runner\u2019s own report, not its exit code"
|
|
1067
1319
|
};
|
|
1068
|
-
var WIDTH =
|
|
1320
|
+
var WIDTH = 8;
|
|
1069
1321
|
var lineOf = (one) => ` ${one.verdict.padEnd(WIDTH)} ${one.subject}: ${one.message}`;
|
|
1070
1322
|
var sectionOf = (check, findings) => {
|
|
1071
1323
|
const mine = findings.filter((one) => one.check === check);
|
|
@@ -1079,8 +1331,8 @@ var formatDoctor = ({ counts, findings, ok, root }) => [
|
|
|
1079
1331
|
`geonosis-doctor \u2014 ${root}`,
|
|
1080
1332
|
"",
|
|
1081
1333
|
...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.",
|
|
1334
|
+
`${CHECKS.length} checks, ${findings.length} lines: ${counts.OK} ok, ${counts.WARN} warned, ${counts.SKIP} skipped, ${counts.UNJUDGED} unjudged, ${counts.FAIL} failed`,
|
|
1335
|
+
ok ? "doctor PASS \u2014 nothing here says the gates are measuring something other than what they claim." : "doctor FAIL \u2014 a line above is a gate reporting on something other than what it names, or a question this could not ask at all.",
|
|
1084
1336
|
""
|
|
1085
1337
|
].join("\n");
|
|
1086
1338
|
var formatJson = (report) => `${JSON.stringify(report, null, 2)}
|
|
@@ -1097,6 +1349,9 @@ export {
|
|
|
1097
1349
|
readRatchet,
|
|
1098
1350
|
defaultRef,
|
|
1099
1351
|
checkBaseline,
|
|
1352
|
+
DEPLOYED_FILE,
|
|
1353
|
+
NOT_WRITTEN,
|
|
1354
|
+
checkDeployed,
|
|
1100
1355
|
CHECKS,
|
|
1101
1356
|
DoctorError,
|
|
1102
1357
|
resolveFrom,
|