@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.
@@ -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 { cpSync, existsSync as existsSync4, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs";
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 join5 } from "path";
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 finding4 = (subject, verdict, message) => ({
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(join5(tmpdir(), "geonosis-doctor-why-"));
1200
+ const dir = mkdtempSync(join6(tmpdir(), "geonosis-doctor-why-"));
1115
1201
  try {
1116
- const probe = join5(dir, "probe.tsx");
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(join5(tmpdir(), "geonosis-doctor-probe-"));
1193
- const here = join5(dir, "corpus");
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
- unclaimed.push(
1209
- `its probe lands at ${files[0]?.path ?? ""}, which the entry enabling it claims none of: ${layer.files.join(", ")}`
1210
- );
1211
- continue;
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 files) {
1214
- const at = join5(here, file.path);
1215
- if (placed.has(at)) continue;
1216
- placed.add(at);
1217
- mkdirSync(dirname2(at), { recursive: true });
1218
- writeFileSync(at, file.source);
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 reach = corpusOf({
1228
- configA: config.path,
1229
- configB: config.path,
1230
- corpus: here,
1231
- oxlint
1232
- }).reach;
1233
- return { fired: new Set(reach.filter((one) => one.firedInA).map((one) => one.rule)), refused };
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) => finding4(config.relative, verdict, message);
1247
- if (repoCorpus !== void 0 && !existsSync4(repoCorpus)) {
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 (!existsSync4(corpus)) {
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 probes = entry === void 0 ? {} : await probesOf(entry, manifest.plugin).catch(() => ({}));
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
- ...unprobed.map(
1326
- (rule) => `${rule} declares no probe, so ${optionSetsOf(config, rule).every((options) => options.length === 0) ? "its scope" : under(config, rule)} does not reach the corpus`
1327
- ),
1328
- ...inert.map((rule) => `${rule}: ${probed.refused.get(rule) ?? ""}`)
1329
- ];
1330
- if (unplaceable.length > 0) return said("UNJUDGED", named(unplaceable));
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 finding5 = (subject, verdict, message) => ({
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) => finding5(config.relative, verdict, `${specifier}: ${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 finding5(specifier, "FAIL", "no workspace in this tree can resolve it at all");
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 ? finding5(specifier, "OK", `1 copy \u2014 ${listed}`) : finding5(
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(finding5(config.relative, "FAIL", config.error));
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 readFileSync5 } from "fs";
1509
- import { join as join6 } from "path";
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 finding6 = (verdict, subject, message) => ({
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 = readFileSync5(join6(root, GEONOSIS_FILE), "utf8");
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 finding6(
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 finding6(
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 finding6("OK", GEONOSIS_FILE, `the configured sink is "${sink}"`);
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 finding6(
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 finding6(
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 finding6(
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 finding6(
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(readFileSync5(join6(root, file), "utf8"));
1733
+ record = JSON.parse(readFileSync7(join7(root, file), "utf8"));
1597
1734
  } catch (error) {
1598
- return finding6(
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 finding6(
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 ? finding6(
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
- ) : finding6(
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 finding6("OK", GEONOSIS_FILE, `the probe that proves this exporter is "${probe}"`);
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 finding6(
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 finding6(
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
- finding6(
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
- finding6(
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 existsSync5, readFileSync as readFileSync6 } from "fs";
1674
- import { join as join7 } from "path";
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 = join7(root, GEONOSIS_FILE2);
1678
- if (!existsSync5(path)) return void 0;
1814
+ const path = join8(root, GEONOSIS_FILE2);
1815
+ if (!existsSync6(path)) return void 0;
1679
1816
  try {
1680
- const parsed = JSON.parse(readFileSync6(path, "utf8"));
1817
+ const parsed = JSON.parse(readFileSync8(path, "utf8"));
1681
1818
  const declared = parsed.doctor?.corpus;
1682
- return typeof declared === "string" && declared !== "" ? join7(root, declared) : void 0;
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 finding7 = (subject, verdict, message) => ({
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
- finding7(subject, verdict, message)
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,
@@ -3,7 +3,7 @@ import {
3
3
  formatDoctor,
4
4
  formatJson,
5
5
  runDoctor
6
- } from "./chunk-7BRGHYQR.js";
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; `drift` asks
9
- * whether the gate is still there at all; `observability` and `deployed` ask the same question one
10
- * layer out — whether what was reported is what happened.
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-7BRGHYQR.js";
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.0",
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.3.0"
38
+ "@geonosis/lint-parity": "1.4.0"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "oxlint": ">=1.77"