@geonosis/doctor 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-7BRGHYQR.js → chunk-R4OLFNI6.js} +211 -69
- package/dist/doctor-cli.js +8 -1
- package/dist/index.d.ts +27 -5
- package/dist/index.js +7 -1
- package/package.json +2 -2
|
@@ -325,6 +325,7 @@ var CHECKS = [
|
|
|
325
325
|
"exercised",
|
|
326
326
|
"baseline",
|
|
327
327
|
"runner",
|
|
328
|
+
"envelope",
|
|
328
329
|
"drift",
|
|
329
330
|
"observability",
|
|
330
331
|
"deployed"
|
|
@@ -1089,17 +1090,102 @@ var checkDrift = ({
|
|
|
1089
1090
|
];
|
|
1090
1091
|
};
|
|
1091
1092
|
|
|
1093
|
+
// src/envelope.ts
|
|
1094
|
+
import { existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync5 } from "fs";
|
|
1095
|
+
import { join as join5 } from "path";
|
|
1096
|
+
var ENVELOPES_DIR = ".geonosis/envelopes";
|
|
1097
|
+
var NO_ENVELOPES = "no .geonosis/envelopes/*.json \u2014 the tools write one per run, so an absent envelope is a run nobody has made here yet, and it is not a balanced one";
|
|
1098
|
+
var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1099
|
+
var finding4 = (verdict, subject, message) => ({
|
|
1100
|
+
check: "envelope",
|
|
1101
|
+
message,
|
|
1102
|
+
subject,
|
|
1103
|
+
verdict
|
|
1104
|
+
});
|
|
1105
|
+
var lengthOf = (value) => Array.isArray(value) ? value.length : void 0;
|
|
1106
|
+
var NEXT = "Next: re-run the tool that wrote it and read the numbers it prints \u2014 a run that has lost count of its own inputs is a bug in that tool, not in this tree";
|
|
1107
|
+
var judge = (subject, name, parsed) => {
|
|
1108
|
+
if (!isRecord2(parsed)) return finding4("FAIL", subject, `is not an object. ${NEXT}`);
|
|
1109
|
+
const tool = parsed["tool"];
|
|
1110
|
+
if (typeof tool !== "string" || tool.trim() === "") {
|
|
1111
|
+
return finding4(
|
|
1112
|
+
"FAIL",
|
|
1113
|
+
subject,
|
|
1114
|
+
`names no tool, so nothing here says which run it is about. ${NEXT}`
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
if (tool !== name) {
|
|
1118
|
+
return finding4(
|
|
1119
|
+
"FAIL",
|
|
1120
|
+
subject,
|
|
1121
|
+
`is written by "${tool}", not "${name}" \u2014 a tool writing another tool's envelope leaves both of their numbers unattributable. ${NEXT}`
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
const version = parsed["version"];
|
|
1125
|
+
if (typeof version !== "string" || version.trim() === "") {
|
|
1126
|
+
return finding4(
|
|
1127
|
+
"FAIL",
|
|
1128
|
+
subject,
|
|
1129
|
+
`${tool}: names no version, so nothing dates this run and a stale envelope reads exactly like a fresh one. ${NEXT}`
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
const considered = parsed["considered"];
|
|
1133
|
+
const read = parsed["read"];
|
|
1134
|
+
const refused = lengthOf(parsed["refused"]);
|
|
1135
|
+
const excused = lengthOf(parsed["excused"]);
|
|
1136
|
+
if (typeof considered !== "number" || typeof read !== "number" || refused === void 0 || excused === void 0) {
|
|
1137
|
+
return finding4(
|
|
1138
|
+
"FAIL",
|
|
1139
|
+
subject,
|
|
1140
|
+
`${tool}: has no considered/read/refused/excused to check \u2014 the four numbers ARE the envelope, and a file without them measures nothing. ${NEXT}`
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
const accounted = read + refused + excused;
|
|
1144
|
+
return considered === accounted ? finding4(
|
|
1145
|
+
"OK",
|
|
1146
|
+
subject,
|
|
1147
|
+
`${tool} considered ${considered} and accounts for all of them \u2014 ${read} read + ${refused} refused + ${excused} excused`
|
|
1148
|
+
) : finding4(
|
|
1149
|
+
"FAIL",
|
|
1150
|
+
subject,
|
|
1151
|
+
`${tool}: considered ${considered} but accounts for ${accounted} \u2014 ${read} read + ${refused} refused + ${excused} excused. It reported on fewer things than it was handed, and every verdict it printed is over the smaller number. ${NEXT}`
|
|
1152
|
+
);
|
|
1153
|
+
};
|
|
1154
|
+
var checkEnvelopes = ({ root }) => {
|
|
1155
|
+
const dir = join5(root, ENVELOPES_DIR);
|
|
1156
|
+
const files = existsSync4(dir) ? readdirSync3(dir).filter((name) => name.endsWith(".json")).toSorted() : [];
|
|
1157
|
+
if (files.length === 0) return [finding4("SKIP", ENVELOPES_DIR, NO_ENVELOPES)];
|
|
1158
|
+
return files.map((name) => {
|
|
1159
|
+
const subject = `${ENVELOPES_DIR}/${name}`;
|
|
1160
|
+
let parsed;
|
|
1161
|
+
try {
|
|
1162
|
+
parsed = JSON.parse(readFileSync5(join5(dir, name), "utf8"));
|
|
1163
|
+
} catch (error) {
|
|
1164
|
+
return finding4("FAIL", subject, `is not readable JSON: ${error.message}. ${NEXT}`);
|
|
1165
|
+
}
|
|
1166
|
+
return judge(subject, name.replace(/\.json$/, ""), parsed);
|
|
1167
|
+
});
|
|
1168
|
+
};
|
|
1169
|
+
|
|
1092
1170
|
// src/exercised.ts
|
|
1093
1171
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
1094
|
-
import {
|
|
1172
|
+
import {
|
|
1173
|
+
cpSync,
|
|
1174
|
+
existsSync as existsSync5,
|
|
1175
|
+
mkdirSync,
|
|
1176
|
+
mkdtempSync,
|
|
1177
|
+
readFileSync as readFileSync6,
|
|
1178
|
+
rmSync,
|
|
1179
|
+
writeFileSync
|
|
1180
|
+
} from "fs";
|
|
1095
1181
|
import { tmpdir } from "os";
|
|
1096
|
-
import { dirname as dirname2, join as
|
|
1182
|
+
import { dirname as dirname2, join as join6, resolve } from "path";
|
|
1097
1183
|
import { corpusOf, readManifest } from "@geonosis/lint-parity";
|
|
1098
1184
|
var OFF = /* @__PURE__ */ new Set([0, "0", "allow", "off", false]);
|
|
1099
1185
|
var severityOf = (level) => Array.isArray(level) ? level[0] : level;
|
|
1100
1186
|
var enabledRulesOf = (rules, plugin) => Object.keys(rules).filter((id) => id.startsWith(`${plugin}/`) && !OFF.has(severityOf(rules[id]))).toSorted();
|
|
1101
1187
|
var passes = (findings) => !findings.some((one) => one.verdict === "FAIL" || one.verdict === "UNJUDGED");
|
|
1102
|
-
var
|
|
1188
|
+
var finding5 = (subject, verdict, message) => ({
|
|
1103
1189
|
check: "exercised",
|
|
1104
1190
|
message,
|
|
1105
1191
|
subject,
|
|
@@ -1111,9 +1197,9 @@ var refusal = (error) => {
|
|
|
1111
1197
|
return said.length > LIMIT ? `${said.slice(0, LIMIT)}\u2026` : said;
|
|
1112
1198
|
};
|
|
1113
1199
|
var reasonFrom = (config, oxlint) => {
|
|
1114
|
-
const dir = mkdtempSync(
|
|
1200
|
+
const dir = mkdtempSync(join6(tmpdir(), "geonosis-doctor-why-"));
|
|
1115
1201
|
try {
|
|
1116
|
-
const probe =
|
|
1202
|
+
const probe = join6(dir, "probe.tsx");
|
|
1117
1203
|
writeFileSync(probe, "export const probe = 1\n");
|
|
1118
1204
|
const run = spawnSync2(
|
|
1119
1205
|
oxlint,
|
|
@@ -1180,6 +1266,20 @@ var enabledHere = (config, plugin) => [
|
|
|
1180
1266
|
...config.overrides.flatMap((one) => enabledRulesOf(one.rules, plugin))
|
|
1181
1267
|
])
|
|
1182
1268
|
].toSorted();
|
|
1269
|
+
var concreteDirOf = (glob) => {
|
|
1270
|
+
const segments = glob.split("/");
|
|
1271
|
+
const out = [];
|
|
1272
|
+
for (const segment of segments.slice(0, -1)) {
|
|
1273
|
+
if (segment === "**") continue;
|
|
1274
|
+
if (segment === "*") {
|
|
1275
|
+
out.push("geonosis");
|
|
1276
|
+
continue;
|
|
1277
|
+
}
|
|
1278
|
+
if (segment.includes("*") || segment.includes("{")) return void 0;
|
|
1279
|
+
out.push(segment);
|
|
1280
|
+
}
|
|
1281
|
+
return out.join("/");
|
|
1282
|
+
};
|
|
1183
1283
|
var throughProbes = ({
|
|
1184
1284
|
config,
|
|
1185
1285
|
corpus,
|
|
@@ -1189,8 +1289,8 @@ var throughProbes = ({
|
|
|
1189
1289
|
}) => {
|
|
1190
1290
|
const refused = /* @__PURE__ */ new Map();
|
|
1191
1291
|
const placed = /* @__PURE__ */ new Set();
|
|
1192
|
-
const dir = mkdtempSync(
|
|
1193
|
-
const here =
|
|
1292
|
+
const dir = mkdtempSync(join6(tmpdir(), "geonosis-doctor-probe-"));
|
|
1293
|
+
const here = join6(dir, "corpus");
|
|
1194
1294
|
try {
|
|
1195
1295
|
cpSync(corpus, here, { recursive: true });
|
|
1196
1296
|
for (const rule of silent) {
|
|
@@ -1204,18 +1304,34 @@ var throughProbes = ({
|
|
|
1204
1304
|
const files = write(layer.options);
|
|
1205
1305
|
if (files.length === 0) throw new Error("its probe declares no file at all");
|
|
1206
1306
|
const claims = layer.files.length === 0 || files.some((file) => layer.files.some((glob) => matchesGlob(glob, file.path)));
|
|
1307
|
+
let mounted = files;
|
|
1207
1308
|
if (!claims) {
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1309
|
+
const rehomed = layer.files.map((glob) => {
|
|
1310
|
+
const home = concreteDirOf(glob);
|
|
1311
|
+
if (home === void 0) return void 0;
|
|
1312
|
+
const extension = /^\*(\.[\w.]+)$/.exec(glob.split("/").at(-1) ?? "")?.[1];
|
|
1313
|
+
const moved = files.map((file) => {
|
|
1314
|
+
const base = file.path.split("/").at(-1) ?? file.path;
|
|
1315
|
+
const adapted = extension === void 0 || base.endsWith(extension) ? base : base.replace(/\.[^.]+$/, extension);
|
|
1316
|
+
const kept = layer.files.some((one) => matchesGlob(one, `${home}/${base}`));
|
|
1317
|
+
return { ...file, path: `${home}/${kept ? base : adapted}` };
|
|
1318
|
+
});
|
|
1319
|
+
return moved.every((file) => layer.files.some((one) => matchesGlob(one, file.path))) ? moved : void 0;
|
|
1320
|
+
}).find((moved) => moved !== void 0);
|
|
1321
|
+
if (rehomed === void 0) {
|
|
1322
|
+
unclaimed.push(
|
|
1323
|
+
`its probe lands at ${files[0]?.path ?? ""}, which the entry enabling it claims none of (and no claimed path could be synthesized): ${layer.files.join(", ")}`
|
|
1324
|
+
);
|
|
1325
|
+
continue;
|
|
1326
|
+
}
|
|
1327
|
+
mounted = rehomed;
|
|
1212
1328
|
}
|
|
1213
|
-
for (const file of
|
|
1214
|
-
const
|
|
1215
|
-
if (placed.has(
|
|
1216
|
-
placed.add(
|
|
1217
|
-
mkdirSync(dirname2(
|
|
1218
|
-
writeFileSync(
|
|
1329
|
+
for (const file of mounted) {
|
|
1330
|
+
const at2 = join6(here, file.path);
|
|
1331
|
+
if (placed.has(at2)) continue;
|
|
1332
|
+
placed.add(at2);
|
|
1333
|
+
mkdirSync(dirname2(at2), { recursive: true });
|
|
1334
|
+
writeFileSync(at2, file.source);
|
|
1219
1335
|
}
|
|
1220
1336
|
anyPlaced = true;
|
|
1221
1337
|
}
|
|
@@ -1224,13 +1340,28 @@ var throughProbes = ({
|
|
|
1224
1340
|
refused.set(rule, String(error.message));
|
|
1225
1341
|
}
|
|
1226
1342
|
}
|
|
1227
|
-
const
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1343
|
+
const probeConfig = JSON.parse(readFileSync6(config.path, "utf8"));
|
|
1344
|
+
const configDir = dirname2(config.path);
|
|
1345
|
+
probeConfig.jsPlugins = (probeConfig.jsPlugins ?? []).map(
|
|
1346
|
+
(spec) => spec.startsWith(".") || spec.startsWith("/") ? resolve(configDir, spec) : packageDirOf(resolveFrom(configDir, spec), spec)
|
|
1347
|
+
);
|
|
1348
|
+
const at = join6(here, ".oxlintrc-geonosis-probe.json");
|
|
1349
|
+
writeFileSync(at, JSON.stringify(probeConfig, null, 2));
|
|
1350
|
+
const run = spawnSync2(
|
|
1351
|
+
oxlint,
|
|
1352
|
+
["-c", at, "--format", "json", "--no-ignore", "--disable-nested-config", here],
|
|
1353
|
+
{ cwd: dir, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 }
|
|
1354
|
+
);
|
|
1355
|
+
const fired = /* @__PURE__ */ new Set();
|
|
1356
|
+
try {
|
|
1357
|
+
const parsed = JSON.parse(run.stdout || "{}");
|
|
1358
|
+
for (const one of parsed.diagnostics ?? []) {
|
|
1359
|
+
const renamed = /^([\w@./-]+)\((.+)\)$/.exec(one.code ?? "");
|
|
1360
|
+
if (renamed !== null) fired.add(`${renamed[1]}/${renamed[2]}`);
|
|
1361
|
+
}
|
|
1362
|
+
} catch {
|
|
1363
|
+
}
|
|
1364
|
+
return { fired, refused };
|
|
1234
1365
|
} finally {
|
|
1235
1366
|
rmSync(dir, { force: true, recursive: true });
|
|
1236
1367
|
}
|
|
@@ -1243,14 +1374,14 @@ var checkExercised = async ({
|
|
|
1243
1374
|
repoCorpus,
|
|
1244
1375
|
root
|
|
1245
1376
|
}) => {
|
|
1246
|
-
const said = (verdict, message) =>
|
|
1247
|
-
if (repoCorpus !== void 0 && !
|
|
1377
|
+
const said = (verdict, message) => finding5(config.relative, verdict, message);
|
|
1378
|
+
if (repoCorpus !== void 0 && !existsSync5(repoCorpus)) {
|
|
1248
1379
|
return said(
|
|
1249
1380
|
"FAIL",
|
|
1250
1381
|
`geonosis.json declares a reach corpus at ${relativeToRoot(root, repoCorpus)} and there is nothing there \u2014 a corpus that cannot be read is a claim, not evidence`
|
|
1251
1382
|
);
|
|
1252
1383
|
}
|
|
1253
|
-
if (!
|
|
1384
|
+
if (!existsSync5(corpus)) {
|
|
1254
1385
|
return said(
|
|
1255
1386
|
"SKIP",
|
|
1256
1387
|
`the plugin loaded from here ships no corpus at ${relativeToRoot(root, corpus)} \u2014 nothing declares which rules it can be evidence about`
|
|
@@ -1301,7 +1432,8 @@ var checkExercised = async ({
|
|
|
1301
1432
|
if (silent.length === 0) {
|
|
1302
1433
|
return said("OK", `${enabled.length} enabled, ${enabled.length} exercised${where}`);
|
|
1303
1434
|
}
|
|
1304
|
-
const
|
|
1435
|
+
const loadedProbes = entry === void 0 ? null : await probesOf(entry, manifest.plugin).catch(() => null);
|
|
1436
|
+
const probes = loadedProbes ?? {};
|
|
1305
1437
|
const declared = silent.filter((rule) => probes[rule] !== void 0);
|
|
1306
1438
|
const unprobed = silent.filter((rule) => probes[rule] === void 0);
|
|
1307
1439
|
let probed = { fired: /* @__PURE__ */ new Set(), refused: /* @__PURE__ */ new Map() };
|
|
@@ -1321,13 +1453,18 @@ var checkExercised = async ({
|
|
|
1321
1453
|
`${countOf(placed, "fire")} nowhere in the corpus and nothing through the probe each declares either: ${named(placed.map((rule) => under(config, rule)))}`
|
|
1322
1454
|
);
|
|
1323
1455
|
}
|
|
1324
|
-
const unplaceable =
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
),
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1456
|
+
const unplaceable = inert.map((rule) => `${rule}: ${probed.refused.get(rule) ?? ""}`);
|
|
1457
|
+
const kitNote = unprobed.length === 0 || loadedProbes === null ? "" : `the loaded plugin ships no probe for ${unprobed.length === 1 ? "this enabled rule" : `${unprobed.length} enabled rules`} \u2014 the kit's omission, not this repo's (geonosis #131): ${unprobed.join(", ")}`;
|
|
1458
|
+
const unknowable = loadedProbes === null ? unprobed.map(
|
|
1459
|
+
(rule) => `${rule} declares no probe, so ${optionSetsOf(config, rule).every((options) => options.length === 0) ? "its scope" : under(config, rule)} does not reach the corpus`
|
|
1460
|
+
) : [];
|
|
1461
|
+
if (unplaceable.length > 0 || unknowable.length > 0) {
|
|
1462
|
+
return said(
|
|
1463
|
+
"UNJUDGED",
|
|
1464
|
+
named([...unknowable, ...unplaceable]) + (kitNote === "" ? "" : ` \u2014 and ${kitNote}`)
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
if (kitNote !== "") return said("WARN", kitNote);
|
|
1331
1468
|
const through = exercised.length === 1 ? "1 through its declared probe under this repo\u2019s options" : `${exercised.length} through their declared probes under this repo\u2019s options`;
|
|
1332
1469
|
return said("OK", `${enabled.length} enabled, ${enabled.length} exercised${where} \u2014 ${through}`);
|
|
1333
1470
|
};
|
|
@@ -1403,7 +1540,7 @@ var declaredFor = ({
|
|
|
1403
1540
|
}
|
|
1404
1541
|
return void 0;
|
|
1405
1542
|
};
|
|
1406
|
-
var
|
|
1543
|
+
var finding6 = (subject, verdict, message) => ({
|
|
1407
1544
|
check: "loaded",
|
|
1408
1545
|
message,
|
|
1409
1546
|
subject,
|
|
@@ -1420,7 +1557,7 @@ var oneConfig = async ({
|
|
|
1420
1557
|
specifier,
|
|
1421
1558
|
workspaces
|
|
1422
1559
|
}) => {
|
|
1423
|
-
const said = (verdict, message) =>
|
|
1560
|
+
const said = (verdict, message) => finding6(config.relative, verdict, `${specifier}: ${message}`);
|
|
1424
1561
|
let loaded;
|
|
1425
1562
|
try {
|
|
1426
1563
|
loaded = await versionAt(resolveFrom(config.dir, specifier), specifier, root);
|
|
@@ -1472,10 +1609,10 @@ var copiesOf = async ({
|
|
|
1472
1609
|
found.set(at, { from: [labelOf(workspace)], version: await pluginVersionOf(entry) });
|
|
1473
1610
|
}
|
|
1474
1611
|
if (found.size === 0) {
|
|
1475
|
-
return
|
|
1612
|
+
return finding6(specifier, "FAIL", "no workspace in this tree can resolve it at all");
|
|
1476
1613
|
}
|
|
1477
1614
|
const listed = [...found.entries()].map(([at, one]) => `${at} ${one.version} (${one.from.join(", ")})`).join("; ");
|
|
1478
|
-
return found.size === 1 ?
|
|
1615
|
+
return found.size === 1 ? finding6(specifier, "OK", `1 copy \u2014 ${listed}`) : finding6(
|
|
1479
1616
|
specifier,
|
|
1480
1617
|
"WARN",
|
|
1481
1618
|
`${found.size} copies \u2014 ${listed}. Which one oxlint runs depends on which directory its config sits in.`
|
|
@@ -1490,7 +1627,7 @@ var checkLoaded = async ({
|
|
|
1490
1627
|
const specifiers = /* @__PURE__ */ new Set();
|
|
1491
1628
|
for (const config of configs) {
|
|
1492
1629
|
if (config.error !== void 0) {
|
|
1493
|
-
findings.push(
|
|
1630
|
+
findings.push(finding6(config.relative, "FAIL", config.error));
|
|
1494
1631
|
continue;
|
|
1495
1632
|
}
|
|
1496
1633
|
for (const specifier of config.jsPlugins.filter((name) => name.startsWith(SCOPE))) {
|
|
@@ -1505,13 +1642,13 @@ var checkLoaded = async ({
|
|
|
1505
1642
|
};
|
|
1506
1643
|
|
|
1507
1644
|
// src/observability.ts
|
|
1508
|
-
import { readFileSync as
|
|
1509
|
-
import { join as
|
|
1645
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
1646
|
+
import { join as join7 } from "path";
|
|
1510
1647
|
var GEONOSIS_FILE = "geonosis.json";
|
|
1511
1648
|
var REACHES_NOTHING = /* @__PURE__ */ new Set(["console", "memory", "noop", "none", "null", "swallowing"]);
|
|
1512
1649
|
var DEFAULT_MAX_AGE_SECONDS = 3600;
|
|
1513
1650
|
var HEAD_TIMEOUT_MS = 3e3;
|
|
1514
|
-
var
|
|
1651
|
+
var finding7 = (verdict, subject, message) => ({
|
|
1515
1652
|
check: "observability",
|
|
1516
1653
|
message,
|
|
1517
1654
|
subject,
|
|
@@ -1520,7 +1657,7 @@ var finding6 = (verdict, subject, message) => ({
|
|
|
1520
1657
|
var readGeonosis2 = (root) => {
|
|
1521
1658
|
let text;
|
|
1522
1659
|
try {
|
|
1523
|
-
text =
|
|
1660
|
+
text = readFileSync7(join7(root, GEONOSIS_FILE), "utf8");
|
|
1524
1661
|
} catch {
|
|
1525
1662
|
return { present: false };
|
|
1526
1663
|
}
|
|
@@ -1538,25 +1675,25 @@ var readGeonosis2 = (root) => {
|
|
|
1538
1675
|
var exporterFinding = (config) => {
|
|
1539
1676
|
const sink = config.sink;
|
|
1540
1677
|
if (typeof sink !== "string" || sink.trim() === "") {
|
|
1541
|
-
return
|
|
1678
|
+
return finding7(
|
|
1542
1679
|
"FAIL",
|
|
1543
1680
|
GEONOSIS_FILE,
|
|
1544
1681
|
"observability.sink is not set, so nothing here says where errors are supposed to go \u2014 and a repo that cannot name its exporter has not got one"
|
|
1545
1682
|
);
|
|
1546
1683
|
}
|
|
1547
1684
|
if (REACHES_NOTHING.has(sink.toLowerCase())) {
|
|
1548
|
-
return
|
|
1685
|
+
return finding7(
|
|
1549
1686
|
"WARN",
|
|
1550
1687
|
GEONOSIS_FILE,
|
|
1551
1688
|
`the configured sink is "${sink}", which answers ok and reaches nothing. Correct in a dev tree; in a deployed one it is the instrument that cannot fail.`
|
|
1552
1689
|
);
|
|
1553
1690
|
}
|
|
1554
|
-
return
|
|
1691
|
+
return finding7("OK", GEONOSIS_FILE, `the configured sink is "${sink}"`);
|
|
1555
1692
|
};
|
|
1556
1693
|
var reachableFinding = async (config) => {
|
|
1557
1694
|
const endpoint = config.endpoint;
|
|
1558
1695
|
if (typeof endpoint !== "string" || endpoint.trim() === "") {
|
|
1559
|
-
return
|
|
1696
|
+
return finding7(
|
|
1560
1697
|
"SKIP",
|
|
1561
1698
|
GEONOSIS_FILE,
|
|
1562
1699
|
"no observability.endpoint was named, so whether the exporter is reachable was not asked"
|
|
@@ -1566,13 +1703,13 @@ var reachableFinding = async (config) => {
|
|
|
1566
1703
|
const timer = setTimeout(() => controller.abort(), HEAD_TIMEOUT_MS);
|
|
1567
1704
|
try {
|
|
1568
1705
|
const response = await fetch(endpoint, { method: "HEAD", signal: controller.signal });
|
|
1569
|
-
return
|
|
1706
|
+
return finding7(
|
|
1570
1707
|
"OK",
|
|
1571
1708
|
GEONOSIS_FILE,
|
|
1572
1709
|
`${endpoint} is reachable \u2014 it answered ${response.status} to a HEAD`
|
|
1573
1710
|
);
|
|
1574
1711
|
} catch (error) {
|
|
1575
|
-
return
|
|
1712
|
+
return finding7(
|
|
1576
1713
|
"FAIL",
|
|
1577
1714
|
GEONOSIS_FILE,
|
|
1578
1715
|
`${endpoint} is not reachable from here: ${error.message}. Every report this repo sends is going into that.`
|
|
@@ -1584,7 +1721,7 @@ var reachableFinding = async (config) => {
|
|
|
1584
1721
|
var ageFinding = (config, root, now) => {
|
|
1585
1722
|
const file = config.lastEventFile;
|
|
1586
1723
|
if (typeof file !== "string" || file.trim() === "") {
|
|
1587
|
-
return
|
|
1724
|
+
return finding7(
|
|
1588
1725
|
"SKIP",
|
|
1589
1726
|
GEONOSIS_FILE,
|
|
1590
1727
|
"no observability.lastEventFile was configured, so when the last event arrived is not a question anything here can answer. Have the sink write { at, id, sink } on every capture and name the file."
|
|
@@ -1593,27 +1730,27 @@ var ageFinding = (config, root, now) => {
|
|
|
1593
1730
|
const maxAgeSeconds = typeof config.maxAgeSeconds === "number" && config.maxAgeSeconds > 0 ? config.maxAgeSeconds : DEFAULT_MAX_AGE_SECONDS;
|
|
1594
1731
|
let record;
|
|
1595
1732
|
try {
|
|
1596
|
-
record = JSON.parse(
|
|
1733
|
+
record = JSON.parse(readFileSync7(join7(root, file), "utf8"));
|
|
1597
1734
|
} catch (error) {
|
|
1598
|
-
return
|
|
1735
|
+
return finding7(
|
|
1599
1736
|
"FAIL",
|
|
1600
1737
|
file,
|
|
1601
1738
|
`the last event file could not be read: ${error.message}. A sink that has never written one has never captured anything.`
|
|
1602
1739
|
);
|
|
1603
1740
|
}
|
|
1604
1741
|
if (typeof record.at !== "number" || !Number.isFinite(record.at)) {
|
|
1605
|
-
return
|
|
1742
|
+
return finding7(
|
|
1606
1743
|
"FAIL",
|
|
1607
1744
|
file,
|
|
1608
1745
|
'the last event record has no numeric "at", so its age cannot be read \u2014 and an age nobody can read is not an age inside the window'
|
|
1609
1746
|
);
|
|
1610
1747
|
}
|
|
1611
1748
|
const ageSeconds = Math.round((now - record.at) / 1e3);
|
|
1612
|
-
return ageSeconds > maxAgeSeconds ?
|
|
1749
|
+
return ageSeconds > maxAgeSeconds ? finding7(
|
|
1613
1750
|
"FAIL",
|
|
1614
1751
|
file,
|
|
1615
1752
|
`the last event arrived ${ageSeconds}s ago, past the ${maxAgeSeconds}s window. An exporter that stopped, a key that was rotated and a sink that has been dropping since Tuesday all look exactly like this, and all of them leave a green build.`
|
|
1616
|
-
) :
|
|
1753
|
+
) : finding7(
|
|
1617
1754
|
"OK",
|
|
1618
1755
|
file,
|
|
1619
1756
|
`the last event arrived ${ageSeconds}s ago, inside the ${maxAgeSeconds}s window`
|
|
@@ -1622,16 +1759,16 @@ var ageFinding = (config, root, now) => {
|
|
|
1622
1759
|
var probeFinding = (config) => {
|
|
1623
1760
|
const probe = config.probe;
|
|
1624
1761
|
if (typeof probe === "string" && probe.trim() !== "") {
|
|
1625
|
-
return
|
|
1762
|
+
return finding7("OK", GEONOSIS_FILE, `the probe that proves this exporter is "${probe}"`);
|
|
1626
1763
|
}
|
|
1627
1764
|
if (typeof config.lastEventFile === "string" && config.lastEventFile.trim() !== "") {
|
|
1628
|
-
return
|
|
1765
|
+
return finding7(
|
|
1629
1766
|
"OK",
|
|
1630
1767
|
GEONOSIS_FILE,
|
|
1631
1768
|
"no probe command, but a last event file is read above, so something does look at this exporter"
|
|
1632
1769
|
);
|
|
1633
1770
|
}
|
|
1634
|
-
return
|
|
1771
|
+
return finding7(
|
|
1635
1772
|
"WARN",
|
|
1636
1773
|
GEONOSIS_FILE,
|
|
1637
1774
|
"neither observability.probe nor observability.lastEventFile is configured, so nothing in this repo has ever established that a report reaches the sink. Name a probe command \u2014 the doctor reports it, your gate runs it."
|
|
@@ -1644,7 +1781,7 @@ var checkObservability = async ({
|
|
|
1644
1781
|
const read = readGeonosis2(root);
|
|
1645
1782
|
if (read.error !== void 0) {
|
|
1646
1783
|
return [
|
|
1647
|
-
|
|
1784
|
+
finding7(
|
|
1648
1785
|
"FAIL",
|
|
1649
1786
|
GEONOSIS_FILE,
|
|
1650
1787
|
`${GEONOSIS_FILE} could not be parsed: ${read.error}. A config nobody can read has not been read, and every question below would have been answered from a default nobody chose.`
|
|
@@ -1653,7 +1790,7 @@ var checkObservability = async ({
|
|
|
1653
1790
|
}
|
|
1654
1791
|
if (!read.present || read.config === void 0) {
|
|
1655
1792
|
return [
|
|
1656
|
-
|
|
1793
|
+
finding7(
|
|
1657
1794
|
"SKIP",
|
|
1658
1795
|
GEONOSIS_FILE,
|
|
1659
1796
|
`no observability block in ${GEONOSIS_FILE}, so nothing here knows where this repo sends its errors. Add { sink, endpoint, lastEventFile | probe, maxAgeSeconds } to have this asked.`
|
|
@@ -1670,16 +1807,16 @@ var checkObservability = async ({
|
|
|
1670
1807
|
};
|
|
1671
1808
|
|
|
1672
1809
|
// src/repo-corpus.ts
|
|
1673
|
-
import { existsSync as
|
|
1674
|
-
import { join as
|
|
1810
|
+
import { existsSync as existsSync6, readFileSync as readFileSync8 } from "fs";
|
|
1811
|
+
import { join as join8 } from "path";
|
|
1675
1812
|
var GEONOSIS_FILE2 = "geonosis.json";
|
|
1676
1813
|
var repoCorpusOf = (root) => {
|
|
1677
|
-
const path =
|
|
1678
|
-
if (!
|
|
1814
|
+
const path = join8(root, GEONOSIS_FILE2);
|
|
1815
|
+
if (!existsSync6(path)) return void 0;
|
|
1679
1816
|
try {
|
|
1680
|
-
const parsed = JSON.parse(
|
|
1817
|
+
const parsed = JSON.parse(readFileSync8(path, "utf8"));
|
|
1681
1818
|
const declared = parsed.doctor?.corpus;
|
|
1682
|
-
return typeof declared === "string" && declared !== "" ?
|
|
1819
|
+
return typeof declared === "string" && declared !== "" ? join8(root, declared) : void 0;
|
|
1683
1820
|
} catch {
|
|
1684
1821
|
return void 0;
|
|
1685
1822
|
}
|
|
@@ -1690,7 +1827,7 @@ var TEST_FAILURES = "testFailures";
|
|
|
1690
1827
|
var RUNS_A_RUNNER = /(?:^|[\s;&|(])(?:npx\s+|bunx\s+|pnpm\s+(?:exec\s+)?)?(?:vitest|bun\s+test)(?:\s|$)/;
|
|
1691
1828
|
var RUNS_BUN_TEST = /(?:^|[\s;&|(])(?:bunx\s+)?bun\s+test(?:\s|$)/;
|
|
1692
1829
|
var WRITES_A_REPORT = /--reporter[= ]\S*json|--outputFile/i;
|
|
1693
|
-
var
|
|
1830
|
+
var finding8 = (subject, verdict, message) => ({
|
|
1694
1831
|
check: "runner",
|
|
1695
1832
|
message,
|
|
1696
1833
|
subject,
|
|
@@ -1718,7 +1855,7 @@ var checkRunner = ({
|
|
|
1718
1855
|
if (script === "") return [];
|
|
1719
1856
|
const subject = workspace.relative === "" ? "package.json" : `${workspace.relative}/package.json`;
|
|
1720
1857
|
const said = (verdict, message) => [
|
|
1721
|
-
|
|
1858
|
+
finding8(subject, verdict, message)
|
|
1722
1859
|
];
|
|
1723
1860
|
if (!RUNS_A_RUNNER.test(script)) {
|
|
1724
1861
|
return said(
|
|
@@ -1838,6 +1975,7 @@ var runDoctor = async ({
|
|
|
1838
1975
|
],
|
|
1839
1976
|
["baseline", () => baselineOf({ baseline, root })],
|
|
1840
1977
|
["runner", () => checkRunner({ ratchet: readRatchet(root), workspaces })],
|
|
1978
|
+
["envelope", () => checkEnvelopes({ root })],
|
|
1841
1979
|
["observability", () => checkObservability({ now: Date.now(), root })],
|
|
1842
1980
|
["drift", () => checkDrift({ root, workspaces })],
|
|
1843
1981
|
["deployed", () => checkDeployed({ root })]
|
|
@@ -1859,6 +1997,7 @@ var ABOUT = {
|
|
|
1859
1997
|
baseline: "a number that may only shrink, against another ref",
|
|
1860
1998
|
deployed: "what the pipeline reported deploying is what the tree declares",
|
|
1861
1999
|
drift: "the gates that were set up and are no longer running",
|
|
2000
|
+
envelope: "every gate read as many things as it was handed",
|
|
1862
2001
|
exercised: "every enabled rule fires on at least one corpus file",
|
|
1863
2002
|
loaded: "the plugin oxlint would load is the one the manifest pins",
|
|
1864
2003
|
observability: "an exporter is configured, reachable, and something arrived through it lately",
|
|
@@ -1912,6 +2051,9 @@ export {
|
|
|
1912
2051
|
relativeToRoot,
|
|
1913
2052
|
READERS,
|
|
1914
2053
|
checkDrift,
|
|
2054
|
+
ENVELOPES_DIR,
|
|
2055
|
+
NO_ENVELOPES,
|
|
2056
|
+
checkEnvelopes,
|
|
1915
2057
|
enabledRulesOf,
|
|
1916
2058
|
checkExercised,
|
|
1917
2059
|
SCOPE,
|
package/dist/doctor-cli.js
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
formatDoctor,
|
|
4
4
|
formatJson,
|
|
5
5
|
runDoctor
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-R4OLFNI6.js";
|
|
7
7
|
|
|
8
8
|
// src/doctor-cli.ts
|
|
9
9
|
import { resolve } from "path";
|
|
@@ -25,6 +25,13 @@ var ABOUT = {
|
|
|
25
25
|
through pnpm/npx/bunx and therefore cannot start, a law over the ceiling its own
|
|
26
26
|
geonosis.json declared, nothing enabling the plugin in either scope, and a
|
|
27
27
|
geonosis.json block no manifest declares a package for (or the reverse).`,
|
|
28
|
+
envelope: `The arithmetic under every other line: for each .geonosis/envelopes/<tool>.json a gate
|
|
29
|
+
wrote, considered === read + refused + excused. Four denominator bugs landed in one
|
|
30
|
+
day and every one of them was GREEN \u2014 a migrations run that reported on three of four
|
|
31
|
+
files, a plan check that printed "PASS \u2014 0 plan(s)" over a directory of twenty-one, a
|
|
32
|
+
parity run over a tree the second config ignored. Each published a numerator and no
|
|
33
|
+
denominator. Reads the files and imports nothing of the tools that wrote them; no
|
|
34
|
+
envelopes at all is a SKIP with the sentence.`,
|
|
28
35
|
exercised: `Every rule a config enables, against the corpus the loaded plugin ships. A rule at
|
|
29
36
|
"error" that can never fire is indistinguishable from a clean tree.`,
|
|
30
37
|
loaded: `The plugin oxlint would LOAD from each config's directory, against the version that
|
package/dist/index.d.ts
CHANGED
|
@@ -5,11 +5,12 @@ type Overrides = {
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* The questions a bump is not finished until something has asked, in the order a run asks them.
|
|
8
|
-
* The first four are ways enforcement has reported green while measuring nothing; `
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* The first four are ways enforcement has reported green while measuring nothing; `envelope` asks
|
|
9
|
+
* the arithmetic underneath all of them — whether a gate read as many things as it was handed;
|
|
10
|
+
* `drift` asks whether the gate is still there at all; `observability` and `deployed` ask the same
|
|
11
|
+
* question one layer out — whether what was reported is what happened.
|
|
11
12
|
*/
|
|
12
|
-
declare const CHECKS: readonly ["loaded", "exercised", "baseline", "runner", "drift", "observability", "deployed"];
|
|
13
|
+
declare const CHECKS: readonly ["loaded", "exercised", "baseline", "runner", "envelope", "drift", "observability", "deployed"];
|
|
13
14
|
type CheckName = (typeof CHECKS)[number];
|
|
14
15
|
/**
|
|
15
16
|
* `SKIP` is a first-class answer and is printed like any other: a check whose line is missing reads
|
|
@@ -163,6 +164,27 @@ declare const checkDrift: ({ readers, root, userSettings, workspaces, }: {
|
|
|
163
164
|
workspaces: Workspace[];
|
|
164
165
|
}) => Finding[];
|
|
165
166
|
|
|
167
|
+
/**
|
|
168
|
+
* The one line that catches a denominator bug: for every envelope a tool wrote,
|
|
169
|
+
* `considered === read + refused + excused`.
|
|
170
|
+
*
|
|
171
|
+
* Four of them landed in one day, all green — a migrations run that reported on three of four
|
|
172
|
+
* files, a plan check that printed `PASS — 0 plan(s)` over a directory of twenty-one, a parity run
|
|
173
|
+
* over a tree the second config had ignored, a validator that walked a list it had already
|
|
174
|
+
* filtered. Every one published a numerator and no denominator, so nothing could be wrong.
|
|
175
|
+
*
|
|
176
|
+
* This READS the files and imports nothing of the tools that wrote them — the same discipline
|
|
177
|
+
* `deployed` and `observability` follow. A check that needed the package it checks cannot run in
|
|
178
|
+
* the tree where that package is missing, which is the first case it exists to find; and this one
|
|
179
|
+
* has to work over a consumer's `.geonosis/` with no kit installed at all. The arithmetic is
|
|
180
|
+
* duplicated here on purpose: it is one comparison over three numbers, and it is the check.
|
|
181
|
+
*/
|
|
182
|
+
declare const ENVELOPES_DIR = ".geonosis/envelopes";
|
|
183
|
+
declare const NO_ENVELOPES = "no .geonosis/envelopes/*.json \u2014 the tools write one per run, so an absent envelope is a run nobody has made here yet, and it is not a balanced one";
|
|
184
|
+
declare const checkEnvelopes: ({ root }: {
|
|
185
|
+
root: string;
|
|
186
|
+
}) => Finding[];
|
|
187
|
+
|
|
166
188
|
/**
|
|
167
189
|
* The rules of one plugin a config actually turns on. A rule NAMED in a config is not a rule
|
|
168
190
|
* enabled by it — `"off"` is how a config records a rule it decided against, and counting those as
|
|
@@ -291,4 +313,4 @@ declare const checkRunner: ({ ratchet, workspaces, }: {
|
|
|
291
313
|
workspaces: Workspace[];
|
|
292
314
|
}) => Finding[];
|
|
293
315
|
|
|
294
|
-
export { CHECKS, CONFIG_FILE, type CheckName, DEPLOYED_FILE, type DiscoveredConfig, DoctorError, type DoctorOptions, type DoctorReport, type Finding, GEONOSIS_FILE, type LastEventRecord, MANIFEST_FILE, type Manifest, NOT_WRITTEN, type ObservabilityConfig, RATCHET_FILE, READERS, type RatchetConfig, SCOPE, type Verdict, type Workspace, checkBaseline, checkDeployed, checkDrift, checkExercised, checkLoaded, checkObservability, checkRunner, corpusOfPlugin, declaredFor, defaultRef, discoverConfigs, discoverWorkspaces, enabledRulesOf, formatDoctor, formatJson, packageDirOf, pluginVersionOf, readConfig, readRatchet, relativePath, relativeToRoot, repoCorpusOf, resolveFrom, runDoctor, satisfies };
|
|
316
|
+
export { CHECKS, CONFIG_FILE, type CheckName, DEPLOYED_FILE, type DiscoveredConfig, DoctorError, type DoctorOptions, type DoctorReport, ENVELOPES_DIR, type Finding, GEONOSIS_FILE, type LastEventRecord, MANIFEST_FILE, type Manifest, NOT_WRITTEN, NO_ENVELOPES, type ObservabilityConfig, RATCHET_FILE, READERS, type RatchetConfig, SCOPE, type Verdict, type Workspace, checkBaseline, checkDeployed, checkDrift, checkEnvelopes, checkExercised, checkLoaded, checkObservability, checkRunner, corpusOfPlugin, declaredFor, defaultRef, discoverConfigs, discoverWorkspaces, enabledRulesOf, formatDoctor, formatJson, packageDirOf, pluginVersionOf, readConfig, readRatchet, relativePath, relativeToRoot, repoCorpusOf, resolveFrom, runDoctor, satisfies };
|
package/dist/index.js
CHANGED
|
@@ -3,15 +3,18 @@ import {
|
|
|
3
3
|
CONFIG_FILE,
|
|
4
4
|
DEPLOYED_FILE,
|
|
5
5
|
DoctorError,
|
|
6
|
+
ENVELOPES_DIR,
|
|
6
7
|
GEONOSIS_FILE,
|
|
7
8
|
MANIFEST_FILE,
|
|
8
9
|
NOT_WRITTEN,
|
|
10
|
+
NO_ENVELOPES,
|
|
9
11
|
RATCHET_FILE,
|
|
10
12
|
READERS,
|
|
11
13
|
SCOPE,
|
|
12
14
|
checkBaseline,
|
|
13
15
|
checkDeployed,
|
|
14
16
|
checkDrift,
|
|
17
|
+
checkEnvelopes,
|
|
15
18
|
checkExercised,
|
|
16
19
|
checkLoaded,
|
|
17
20
|
checkObservability,
|
|
@@ -34,21 +37,24 @@ import {
|
|
|
34
37
|
resolveFrom,
|
|
35
38
|
runDoctor,
|
|
36
39
|
satisfies
|
|
37
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-R4OLFNI6.js";
|
|
38
41
|
export {
|
|
39
42
|
CHECKS,
|
|
40
43
|
CONFIG_FILE,
|
|
41
44
|
DEPLOYED_FILE,
|
|
42
45
|
DoctorError,
|
|
46
|
+
ENVELOPES_DIR,
|
|
43
47
|
GEONOSIS_FILE,
|
|
44
48
|
MANIFEST_FILE,
|
|
45
49
|
NOT_WRITTEN,
|
|
50
|
+
NO_ENVELOPES,
|
|
46
51
|
RATCHET_FILE,
|
|
47
52
|
READERS,
|
|
48
53
|
SCOPE,
|
|
49
54
|
checkBaseline,
|
|
50
55
|
checkDeployed,
|
|
51
56
|
checkDrift,
|
|
57
|
+
checkEnvelopes,
|
|
52
58
|
checkExercised,
|
|
53
59
|
checkLoaded,
|
|
54
60
|
checkObservability,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geonosis/doctor",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"types": "./dist/index.d.ts",
|
|
5
5
|
"description": "The adoption doctor — declared ≠ loaded, enabled ≠ exercised, a baseline that grew, a runner whose exit code is the only verdict.",
|
|
6
6
|
"keywords": [
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"dist"
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@geonosis/lint-parity": "1.
|
|
38
|
+
"@geonosis/lint-parity": "1.4.0"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"oxlint": ">=1.77"
|