@geonosis/doctor 2.1.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # @geonosis/doctor
2
2
 
3
+ Through the front door: `geonosis doctor` — the metapackage pins this and every other kit tool at
4
+ ONE version, and passes the exit code through unchanged.
5
+
3
6
  Seven questions a version bump is not finished until something has asked. The first four are ways
4
7
  enforcement has reported **green while measuring nothing** — in real repos, for weeks at a time.
5
8
  `drift` asks whether the gate is still there at all. The last two ask the same question one layer
@@ -24,9 +27,9 @@ geonosis-doctor [--root <dir>] [--json] [--strict] [--baseline-against [<ref>]]
24
27
 
25
28
  ## One check, asked alone
26
29
 
27
- `geonosis-doctor --only loaded` runs that check and no other, in well under a second: a manifest read and a resolution per config. It exists because the doctor subsumed a consumer's own declared-vs-loaded script that lived in a 2.5 s local gate, and eleven seconds for seven checks cannot live there — so the question that catches a stale nested copy earliest had fallen out of the gate that runs most often (measured in a consumer, 2026-08-30: a per-workspace `bun add` left one workspace loading 1.0.0 against configs written for 1.1.0, and the script about to be deleted caught it). `--only loaded,runner` takes a list; a name the doctor does not have is refused with the seven it does. A `loaded` FAIL now says the remedy: remove the nested copies and reinstall.
30
+ `geonosis-doctor --only loaded` runs that check and no other, in well under a second: a manifest read and a resolution per config. It exists because the doctor subsumed a consumer's own declared-vs-loaded script that lived in a 2.5 s local gate, and eleven seconds for every check cannot live there — so the question that catches a stale nested copy earliest had fallen out of the gate that runs most often (measured in a consumer, 2026-08-30: a per-workspace `bun add` left one workspace loading 1.0.0 against configs written for 1.1.0, and the script about to be deleted caught it). `--only loaded,runner` takes a list; a name the doctor does not have is refused with the ones it does. A `loaded` FAIL now says the remedy: remove the nested copies and reinstall.
28
31
 
29
- ## The seven checks
32
+ ## The eleven checks
30
33
 
31
34
  ### `loaded` — declared ≠ loaded
32
35
 
@@ -326,6 +329,26 @@ environments inherit nothing per binding) and `release.secrets`.
326
329
  It reads the file, and imports nothing of `@geonosis/release` — the same wall `observability` keeps,
327
330
  held by the kit's own `layer-walls` config, so a repo can install either package alone.
328
331
 
332
+ ### `exams` — a floor adopted and never examined
333
+
334
+ A floor bump that breaks search is invisible to a tier that never runs the search exam. A verify
335
+ tier declares COMMANDS, not globs, so "the exam runs in the fast tier" is not a claim anything can
336
+ check — but whether a test file importing the exam exists at all is.
337
+
338
+ For every `@geonosis/*` package a manifest in the tree declares, this reads the declarations that
339
+ package ships and takes every export ending in `Conformance` as its exam. The exams are read off
340
+ the installed floor, never off a list held here: a table in this package would be a second source
341
+ for a fact the floors already state, and wrong the day a floor adds one.
342
+
343
+ | | |
344
+ |---|---|
345
+ | no declared package ships an exam | **SKIP** — nothing here to ask about |
346
+ | a test file imports one of the floor's exams | **OK**, naming the file and the exams it runs |
347
+ | the floor is declared and no test file imports any of its exams | **WARN**, naming the floor and every exam it ships |
348
+
349
+ It WARNs rather than FAILs: the check cannot tell a floor adopted last week from one whose exam
350
+ somebody deleted, and a FAIL on every fresh install is the shape that teaches a reader to skim.
351
+
329
352
  ## What `--format=unix` does not tell you
330
353
 
331
354
  Measured on oxlint 1.80: a JS plugin rule that throws from `create()` is reported **once per file**,
@@ -149,10 +149,14 @@ var checkBaseline = ({ ref, root }) => {
149
149
  )
150
150
  ];
151
151
  }
152
- const grew = Object.entries(there).filter(([key, was]) => (here[key] ?? was) > was).map(
153
- ([key, was]) => finding(path, "FAIL", `${key} ${was} \u2192 ${here[key] ?? was} (grew against ${ref})`)
154
- );
155
- return grew.length > 0 ? grew : [
152
+ const regressed = Object.entries(there).flatMap(([key, was]) => {
153
+ const now = here[key];
154
+ if (now === void 0) {
155
+ return [finding(path, "FAIL", `${key} ${was} \u2192 gone (removed from ${path} against ${ref})`)];
156
+ }
157
+ return now > was ? [finding(path, "FAIL", `${key} ${was} \u2192 ${now} (grew against ${ref})`)] : [];
158
+ });
159
+ return regressed.length > 0 ? regressed : [
156
160
  finding(
157
161
  path,
158
162
  "OK",
@@ -346,7 +350,8 @@ var CHECKS = [
346
350
  "drift",
347
351
  "observability",
348
352
  "deployed",
349
- "rails"
353
+ "rails",
354
+ "exams"
350
355
  ];
351
356
  var DoctorError = class extends Error {
352
357
  constructor(message) {
@@ -423,10 +428,28 @@ var probesOf = async (entry, plugin) => {
423
428
  })
424
429
  );
425
430
  };
431
+ var grantsOf = async (entry) => {
432
+ const loaded = await import(pathToFileURL(entry).href);
433
+ const plugin = loaded.default?.meta?.name ?? "";
434
+ return Object.fromEntries(
435
+ Object.entries(loaded.default?.rules ?? {}).flatMap(([name, rule]) => {
436
+ const grants = rule?.meta?.grants;
437
+ return typeof grants?.modules === "string" && typeof grants.paths === "string" ? [[`${plugin}/${name}`, { modules: grants.modules, paths: grants.paths }]] : [];
438
+ })
439
+ );
440
+ };
426
441
  var presumptionsOf = async (entry) => {
427
442
  const loaded = await import(pathToFileURL(entry).href);
428
443
  const namespace = loaded.default?.meta?.name;
429
444
  return {
445
+ // #290: a rule that names a pattern whose floor is not shipped is a requirement with nothing to
446
+ // install behind it — the second consumer hand-writes the floor to satisfy the lint.
447
+ floors: Object.fromEntries(
448
+ Object.entries(loaded.default?.rules ?? {}).flatMap(([name, rule]) => {
449
+ const floor = rule?.meta?.floor;
450
+ return typeof floor?.name === "string" && floor.pending === true ? [[name, { name: floor.name }]] : [];
451
+ })
452
+ ),
430
453
  namespace: typeof namespace === "string" ? namespace : "",
431
454
  presumed: Object.fromEntries(
432
455
  Object.entries(loaded.default?.rules ?? {}).flatMap(([name, rule]) => {
@@ -1217,6 +1240,10 @@ var COMPOSABLE = [
1217
1240
  }))
1218
1241
  ];
1219
1242
  var SOURCE_FILE = /\.[cm]?[jt]sx?$/;
1243
+ var QUOTE = "['\"`]";
1244
+ var imports = (body, name) => new RegExp(
1245
+ `(?:from|import|require)\\s*\\(?\\s*${QUOTE}${name.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`)}(?:/[^'"\`]*)?${QUOTE}`
1246
+ ).test(body);
1220
1247
  var composedHere = (root, workspaces) => {
1221
1248
  const declared = declaredAnywhere(workspaces);
1222
1249
  const wanted = COMPOSABLE.filter((one) => declared.has(one.name));
@@ -1230,7 +1257,7 @@ var composedHere = (root, workspaces) => {
1230
1257
  continue;
1231
1258
  }
1232
1259
  for (const one of wanted) {
1233
- if (!importing.has(one.name) && body.includes(one.name)) importing.set(one.name, path);
1260
+ if (!importing.has(one.name) && imports(body, one.name)) importing.set(one.name, path);
1234
1261
  }
1235
1262
  if (importing.size === wanted.length) break;
1236
1263
  }
@@ -1332,7 +1359,7 @@ var blocks = (root, config, readers, workspaces) => {
1332
1359
  finding4(
1333
1360
  name,
1334
1361
  "WARN",
1335
- `${name} is installed and geonosis.json has no "${block}" block \u2014 it runs on its defaults, whatever they are`
1362
+ `${name} is installed and geonosis.json has no "${block}" block \u2014 it runs on its own defaults; \`geonosis explain ${name}\` prints them beside the files this repo has`
1336
1363
  )
1337
1364
  ];
1338
1365
  }
@@ -1895,10 +1922,154 @@ var checkEnvelopes = ({ root }) => {
1895
1922
  });
1896
1923
  };
1897
1924
 
1898
- // src/loaded.ts
1899
- import { readFileSync as readFileSync10 } from "fs";
1900
- import { join as join10, sep as sep3 } from "path";
1925
+ // src/exams.ts
1926
+ import { existsSync as existsSync9, readFileSync as readFileSync10 } from "fs";
1927
+ import { dirname as dirname3, join as join10 } from "path";
1901
1928
  var SCOPE = "@geonosis/";
1929
+ var EXAM = /(?:^|[^$\w])([$\w]*Conformance)\b/g;
1930
+ var finding7 = (subject, verdict, message) => ({
1931
+ check: "exams",
1932
+ message,
1933
+ subject,
1934
+ verdict
1935
+ });
1936
+ var declaredBy = (workspaces) => {
1937
+ const found = /* @__PURE__ */ new Map();
1938
+ const provided = new Set(
1939
+ workspaces.map((one) => one.manifest.name).filter((one) => typeof one === "string")
1940
+ );
1941
+ for (const workspace of workspaces) {
1942
+ const manifest = workspace.manifest;
1943
+ for (const block of ["dependencies", "devDependencies", "peerDependencies"]) {
1944
+ const declared = manifest[block];
1945
+ if (typeof declared !== "object" || declared === null) continue;
1946
+ for (const name of Object.keys(declared)) {
1947
+ if (name.startsWith(SCOPE) && !found.has(name) && !provided.has(name)) {
1948
+ found.set(name, workspace.dir);
1949
+ }
1950
+ }
1951
+ }
1952
+ }
1953
+ return [...found].map(([name, from]) => ({ from, name })).toSorted((a, b) => a.name.localeCompare(b.name));
1954
+ };
1955
+ var declarationsOf = (dir) => {
1956
+ const at = join10(dir, "package.json");
1957
+ if (!existsSync9(at)) return "";
1958
+ const manifest = JSON.parse(readFileSync10(at, "utf8"));
1959
+ const entries = [];
1960
+ const walk2 = (value) => {
1961
+ if (typeof value === "string" && value.endsWith(".d.ts")) entries.push(value);
1962
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
1963
+ for (const one of Object.values(value)) walk2(one);
1964
+ }
1965
+ };
1966
+ walk2({ exports: manifest["exports"], types: manifest["types"] });
1967
+ return [...new Set(entries)].map((entry) => {
1968
+ try {
1969
+ return readFileSync10(join10(dir, entry), "utf8");
1970
+ } catch {
1971
+ return "";
1972
+ }
1973
+ }).join("\n");
1974
+ };
1975
+ var installedAt = (from, name) => {
1976
+ let at = from;
1977
+ for (; ; ) {
1978
+ const dir = join10(at, "node_modules", name);
1979
+ if (existsSync9(join10(dir, "package.json"))) return dir;
1980
+ const up = dirname3(at);
1981
+ if (up === at) return void 0;
1982
+ at = up;
1983
+ }
1984
+ };
1985
+ var examsShippedBy = (from, name) => {
1986
+ const dir = installedAt(from, name);
1987
+ if (dir === void 0) return [];
1988
+ return [...new Set([...declarationsOf(dir).matchAll(EXAM)].map((one) => one[1]))];
1989
+ };
1990
+ var checkExams = ({
1991
+ root,
1992
+ workspaces
1993
+ }) => {
1994
+ const tests = testFilesUnder(root).map((path) => ({ path, source: readFileSync10(path, "utf8") }));
1995
+ const found = [];
1996
+ for (const { from, name } of declaredBy(workspaces)) {
1997
+ const exams = examsShippedBy(from, name);
1998
+ if (exams.length === 0) continue;
1999
+ const importing = tests.find(
2000
+ (one) => one.source.includes(name) && exams.some((exam) => one.source.includes(exam))
2001
+ );
2002
+ found.push(
2003
+ importing === void 0 ? finding7(
2004
+ name,
2005
+ "WARN",
2006
+ `this tree declares ${name} and no test file imports its exam \u2014 ${exams.join(", ")}. A floor bump that broke it would read exactly like a green tier: the exam is the only thing that asks.`
2007
+ ) : finding7(
2008
+ name,
2009
+ "OK",
2010
+ `${relativePath(root, importing.path)} runs its exam (${exams.filter((exam) => importing.source.includes(exam)).join(", ")})`
2011
+ )
2012
+ );
2013
+ }
2014
+ return found.length > 0 ? found : [
2015
+ finding7(
2016
+ "package.json",
2017
+ "SKIP",
2018
+ `no floor with an exam is declared here \u2014 nothing to ask about, and nothing this can pass`
2019
+ )
2020
+ ];
2021
+ };
2022
+
2023
+ // src/claude-plugin.ts
2024
+ import { existsSync as existsSync10, readFileSync as readFileSync11 } from "fs";
2025
+ import { homedir as homedir2 } from "os";
2026
+ import { join as join11 } from "path";
2027
+ var INSTALLED_PLUGINS = ".claude/plugins/installed_plugins.json";
2028
+ var PLUGIN_NAME = "geonosis";
2029
+ var installedPluginVersions = (home, name) => {
2030
+ const at = join11(home, INSTALLED_PLUGINS);
2031
+ if (!existsSync10(at)) return [];
2032
+ let record;
2033
+ try {
2034
+ record = JSON.parse(readFileSync11(at, "utf8"));
2035
+ } catch {
2036
+ return [];
2037
+ }
2038
+ return Object.entries(record.plugins ?? {}).filter(([key]) => (key.split("@")[0] ?? key) === name).flatMap(([, value]) => Array.isArray(value) ? value : []).flatMap((one) => typeof one.version === "string" ? [one.version] : []);
2039
+ };
2040
+ var checkClaudePlugin = ({
2041
+ home = homedir2(),
2042
+ name = PLUGIN_NAME,
2043
+ train
2044
+ }) => {
2045
+ if (train === void 0) return [];
2046
+ const installed = installedPluginVersions(home, name);
2047
+ if (installed.length === 0) {
2048
+ return [
2049
+ {
2050
+ check: "loaded",
2051
+ message: `this machine has no "${name}" plugin installed, so none of this release's hooks, agents or skills is loaded in any session here \u2014 a bump moved the packages and nothing moved them. Install it, or say out loud that this repo runs without the plugin`,
2052
+ subject: `${name} (Claude plugin)`,
2053
+ verdict: "WARN"
2054
+ }
2055
+ ];
2056
+ }
2057
+ const drifted = installed.filter((one) => one !== train);
2058
+ if (drifted.length === 0) return [];
2059
+ return [
2060
+ {
2061
+ check: "loaded",
2062
+ message: `the installed plugin is ${drifted.join(", ")} and the packages here are ${train} \u2014 the plugin carries the hooks, the agents and the skills, and it moves on \`claude plugin update\`, never on an install. Every session in this repo is running the older ones`,
2063
+ subject: `${name} (Claude plugin)`,
2064
+ verdict: "WARN"
2065
+ }
2066
+ ];
2067
+ };
2068
+
2069
+ // src/loaded.ts
2070
+ import { readFileSync as readFileSync12 } from "fs";
2071
+ import { join as join12, sep as sep3 } from "path";
2072
+ var SCOPE2 = "@geonosis/";
1902
2073
  var NOT_THE_SCOPE_DIRECTORY = "Deleting only the scope directory inside that node_modules is a repair no pnpm install undoes \u2014 the tree is then missing the package and every install stays green.";
1903
2074
  var BLOCKS = [
1904
2075
  "dependencies",
@@ -1968,7 +2139,7 @@ var declaredFor = ({
1968
2139
  }
1969
2140
  return void 0;
1970
2141
  };
1971
- var finding7 = (subject, verdict, message) => ({
2142
+ var finding8 = (subject, verdict, message) => ({
1972
2143
  check: "loaded",
1973
2144
  message,
1974
2145
  subject,
@@ -1985,7 +2156,7 @@ var oneConfig = async ({
1985
2156
  specifier,
1986
2157
  workspaces
1987
2158
  }) => {
1988
- const said2 = (verdict, message) => finding7(config.relative, verdict, `${specifier}: ${message}`);
2159
+ const said2 = (verdict, message) => finding8(config.relative, verdict, `${specifier}: ${message}`);
1989
2160
  let loaded;
1990
2161
  try {
1991
2162
  loaded = await versionAt(resolveFrom(config.dir, specifier), specifier, root);
@@ -2037,10 +2208,10 @@ var copiesOf = async ({
2037
2208
  found.set(at, { from: [labelOf2(workspace)], version: await pluginVersionOf(entry) });
2038
2209
  }
2039
2210
  if (found.size === 0) {
2040
- return finding7(specifier, "FAIL", "no workspace in this tree can resolve it at all");
2211
+ return finding8(specifier, "FAIL", "no workspace in this tree can resolve it at all");
2041
2212
  }
2042
2213
  const listed = [...found.entries()].map(([at, one]) => `${at} ${one.version} (${one.from.join(", ")})`).join("; ");
2043
- return found.size === 1 ? finding7(specifier, "OK", `1 copy \u2014 ${listed}`) : finding7(
2214
+ return found.size === 1 ? finding8(specifier, "OK", `1 copy \u2014 ${listed}`) : finding8(
2044
2215
  specifier,
2045
2216
  "WARN",
2046
2217
  `${found.size} copies \u2014 ${listed}. Which one oxlint runs depends on which directory its config sits in.`
@@ -2050,7 +2221,7 @@ var shipperOf = (root, bin) => {
2050
2221
  const name = bin === "geonosis" ? "@geonosis/cli" : `@geonosis/${bin.replace(/^geonosis-/, "")}`;
2051
2222
  try {
2052
2223
  const manifest = JSON.parse(
2053
- readFileSync10(join10(packageDirOf(resolveFrom(root, name), name), "package.json"), "utf8")
2224
+ readFileSync12(join12(packageDirOf(resolveFrom(root, name), name), "package.json"), "utf8")
2054
2225
  );
2055
2226
  const declared = manifest.bin;
2056
2227
  return typeof declared === "object" && !Object.hasOwn(declared, bin) ? "" : name;
@@ -2065,14 +2236,14 @@ var knipNote = (root, bin) => {
2065
2236
  var gitHooks = (root) => {
2066
2237
  const lines = hookLines(root);
2067
2238
  if (lines.length === 0) {
2068
- return [finding7("git hooks", "SKIP", "no lefthook, husky or .githooks file here to read")];
2239
+ return [finding8("git hooks", "SKIP", "no lefthook, husky or .githooks file here to read")];
2069
2240
  }
2070
2241
  const files = [...new Set(lines.map((one) => one.at))].toSorted();
2071
2242
  return files.flatMap((at) => {
2072
2243
  const here = lines.filter((one) => one.at === at);
2073
2244
  const named2 = here.filter((one) => NAMES_A_BIN.test(one.line));
2074
2245
  if (named2.length === 0) {
2075
- return [finding7(at, "SKIP", "it names no geonosis bin, so there is nothing here to start")];
2246
+ return [finding8(at, "SKIP", "it names no geonosis bin, so there is nothing here to start")];
2076
2247
  }
2077
2248
  const wrong = named2.flatMap((one) => {
2078
2249
  const found = VERSION_MANAGED.exec(one.line)?.groups;
@@ -2080,7 +2251,7 @@ var gitHooks = (root) => {
2080
2251
  });
2081
2252
  if (wrong.length === 0) {
2082
2253
  return [
2083
- finding7(
2254
+ finding8(
2084
2255
  at,
2085
2256
  "OK",
2086
2257
  `${named2.length} geonosis bin(s) here, every one called directly rather than through a runner`
@@ -2088,7 +2259,7 @@ var gitHooks = (root) => {
2088
2259
  ];
2089
2260
  }
2090
2261
  return wrong.map(
2091
- ({ bin, runner }) => finding7(
2262
+ ({ bin, runner }) => finding8(
2092
2263
  at,
2093
2264
  "FAIL",
2094
2265
  `it runs ${bin} through ${runner} \u2014 a git hook's PATH is not the shell's and carries no version-manager shim, so ${runner} fails to START and what the author reads is its message, not this gate's. Call it directly: node_modules/.bin/${bin}${knipNote(root, bin)}`
@@ -2098,6 +2269,7 @@ var gitHooks = (root) => {
2098
2269
  };
2099
2270
  var checkLoaded = async ({
2100
2271
  configs,
2272
+ home,
2101
2273
  root,
2102
2274
  workspaces
2103
2275
  }) => {
@@ -2105,10 +2277,10 @@ var checkLoaded = async ({
2105
2277
  const specifiers = /* @__PURE__ */ new Set();
2106
2278
  for (const config of configs) {
2107
2279
  if (config.error !== void 0) {
2108
- findings.push(finding7(config.relative, "FAIL", config.error));
2280
+ findings.push(finding8(config.relative, "FAIL", config.error));
2109
2281
  continue;
2110
2282
  }
2111
- for (const specifier of config.jsPlugins.filter((name) => name.startsWith(SCOPE))) {
2283
+ for (const specifier of config.jsPlugins.filter((name) => name.startsWith(SCOPE2))) {
2112
2284
  specifiers.add(specifier);
2113
2285
  findings.push(await oneConfig({ config, root, specifier, workspaces }));
2114
2286
  }
@@ -2116,17 +2288,28 @@ var checkLoaded = async ({
2116
2288
  for (const specifier of [...specifiers].toSorted()) {
2117
2289
  findings.push(await copiesOf({ root, specifier, workspaces }));
2118
2290
  }
2291
+ findings.push(...checkClaudePlugin({ home, train: await trainVersion(root, [...specifiers]) }));
2119
2292
  return findings;
2120
2293
  };
2294
+ var trainVersion = async (root, specifiers) => {
2295
+ for (const specifier of specifiers.toSorted()) {
2296
+ try {
2297
+ return await pluginVersionOf(resolveFrom(root, specifier));
2298
+ } catch {
2299
+ continue;
2300
+ }
2301
+ }
2302
+ return void 0;
2303
+ };
2121
2304
 
2122
2305
  // src/observability.ts
2123
- import { readFileSync as readFileSync11 } from "fs";
2124
- import { join as join11 } from "path";
2306
+ import { readFileSync as readFileSync13 } from "fs";
2307
+ import { join as join13 } from "path";
2125
2308
  var GEONOSIS_FILE2 = "geonosis.json";
2126
2309
  var REACHES_NOTHING = /* @__PURE__ */ new Set(["console", "memory", "noop", "none", "null", "swallowing"]);
2127
2310
  var DEFAULT_MAX_AGE_SECONDS = 3600;
2128
2311
  var HEAD_TIMEOUT_MS = 3e3;
2129
- var finding8 = (verdict, subject, message) => ({
2312
+ var finding9 = (verdict, subject, message) => ({
2130
2313
  check: "observability",
2131
2314
  message,
2132
2315
  subject,
@@ -2135,7 +2318,7 @@ var finding8 = (verdict, subject, message) => ({
2135
2318
  var readGeonosis2 = (root) => {
2136
2319
  let text;
2137
2320
  try {
2138
- text = readFileSync11(join11(root, GEONOSIS_FILE2), "utf8");
2321
+ text = readFileSync13(join13(root, GEONOSIS_FILE2), "utf8");
2139
2322
  } catch {
2140
2323
  return { present: false };
2141
2324
  }
@@ -2153,25 +2336,25 @@ var readGeonosis2 = (root) => {
2153
2336
  var exporterFinding = (config) => {
2154
2337
  const sink = config.sink;
2155
2338
  if (typeof sink !== "string" || sink.trim() === "") {
2156
- return finding8(
2339
+ return finding9(
2157
2340
  "FAIL",
2158
2341
  GEONOSIS_FILE2,
2159
2342
  "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"
2160
2343
  );
2161
2344
  }
2162
2345
  if (REACHES_NOTHING.has(sink.toLowerCase())) {
2163
- return finding8(
2346
+ return finding9(
2164
2347
  "WARN",
2165
2348
  GEONOSIS_FILE2,
2166
2349
  `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.`
2167
2350
  );
2168
2351
  }
2169
- return finding8("OK", GEONOSIS_FILE2, `the configured sink is "${sink}"`);
2352
+ return finding9("OK", GEONOSIS_FILE2, `the configured sink is "${sink}"`);
2170
2353
  };
2171
2354
  var reachableFinding = async (config) => {
2172
2355
  const endpoint = config.endpoint;
2173
2356
  if (typeof endpoint !== "string" || endpoint.trim() === "") {
2174
- return finding8(
2357
+ return finding9(
2175
2358
  "SKIP",
2176
2359
  GEONOSIS_FILE2,
2177
2360
  "no observability.endpoint was named, so whether the exporter is reachable was not asked"
@@ -2181,13 +2364,13 @@ var reachableFinding = async (config) => {
2181
2364
  const timer = setTimeout(() => controller.abort(), HEAD_TIMEOUT_MS);
2182
2365
  try {
2183
2366
  const response = await fetch(endpoint, { method: "HEAD", signal: controller.signal });
2184
- return finding8(
2367
+ return finding9(
2185
2368
  "OK",
2186
2369
  GEONOSIS_FILE2,
2187
2370
  `${endpoint} is reachable \u2014 it answered ${response.status} to a HEAD`
2188
2371
  );
2189
2372
  } catch (error) {
2190
- return finding8(
2373
+ return finding9(
2191
2374
  "FAIL",
2192
2375
  GEONOSIS_FILE2,
2193
2376
  `${endpoint} is not reachable from here: ${error.message}. Every report this repo sends is going into that.`
@@ -2199,7 +2382,7 @@ var reachableFinding = async (config) => {
2199
2382
  var ageFinding = (config, root, now) => {
2200
2383
  const file = config.lastEventFile;
2201
2384
  if (typeof file !== "string" || file.trim() === "") {
2202
- return finding8(
2385
+ return finding9(
2203
2386
  "SKIP",
2204
2387
  GEONOSIS_FILE2,
2205
2388
  "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."
@@ -2208,27 +2391,27 @@ var ageFinding = (config, root, now) => {
2208
2391
  const maxAgeSeconds = typeof config.maxAgeSeconds === "number" && config.maxAgeSeconds > 0 ? config.maxAgeSeconds : DEFAULT_MAX_AGE_SECONDS;
2209
2392
  let record;
2210
2393
  try {
2211
- record = JSON.parse(readFileSync11(join11(root, file), "utf8"));
2394
+ record = JSON.parse(readFileSync13(join13(root, file), "utf8"));
2212
2395
  } catch (error) {
2213
- return finding8(
2396
+ return finding9(
2214
2397
  "FAIL",
2215
2398
  file,
2216
2399
  `the last event file could not be read: ${error.message}. A sink that has never written one has never captured anything.`
2217
2400
  );
2218
2401
  }
2219
2402
  if (typeof record.at !== "number" || !Number.isFinite(record.at)) {
2220
- return finding8(
2403
+ return finding9(
2221
2404
  "FAIL",
2222
2405
  file,
2223
2406
  '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'
2224
2407
  );
2225
2408
  }
2226
2409
  const ageSeconds = Math.round((now - record.at) / 1e3);
2227
- return ageSeconds > maxAgeSeconds ? finding8(
2410
+ return ageSeconds > maxAgeSeconds ? finding9(
2228
2411
  "FAIL",
2229
2412
  file,
2230
2413
  `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.`
2231
- ) : finding8(
2414
+ ) : finding9(
2232
2415
  "OK",
2233
2416
  file,
2234
2417
  `the last event arrived ${ageSeconds}s ago, inside the ${maxAgeSeconds}s window`
@@ -2237,16 +2420,16 @@ var ageFinding = (config, root, now) => {
2237
2420
  var probeFinding = (config) => {
2238
2421
  const probe = config.probe;
2239
2422
  if (typeof probe === "string" && probe.trim() !== "") {
2240
- return finding8("OK", GEONOSIS_FILE2, `the probe that proves this exporter is "${probe}"`);
2423
+ return finding9("OK", GEONOSIS_FILE2, `the probe that proves this exporter is "${probe}"`);
2241
2424
  }
2242
2425
  if (typeof config.lastEventFile === "string" && config.lastEventFile.trim() !== "") {
2243
- return finding8(
2426
+ return finding9(
2244
2427
  "OK",
2245
2428
  GEONOSIS_FILE2,
2246
2429
  "no probe command, but a last event file is read above, so something does look at this exporter"
2247
2430
  );
2248
2431
  }
2249
- return finding8(
2432
+ return finding9(
2250
2433
  "WARN",
2251
2434
  GEONOSIS_FILE2,
2252
2435
  "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."
@@ -2259,7 +2442,7 @@ var checkObservability = async ({
2259
2442
  const read = readGeonosis2(root);
2260
2443
  if (read.error !== void 0) {
2261
2444
  return [
2262
- finding8(
2445
+ finding9(
2263
2446
  "FAIL",
2264
2447
  GEONOSIS_FILE2,
2265
2448
  `${GEONOSIS_FILE2} 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.`
@@ -2268,7 +2451,7 @@ var checkObservability = async ({
2268
2451
  }
2269
2452
  if (!read.present || read.config === void 0) {
2270
2453
  return [
2271
- finding8(
2454
+ finding9(
2272
2455
  "SKIP",
2273
2456
  GEONOSIS_FILE2,
2274
2457
  `no observability block in ${GEONOSIS_FILE2}, so nothing here knows where this repo sends its errors. Add { sink, endpoint, lastEventFile | probe, maxAgeSeconds } to have this asked.`
@@ -2285,13 +2468,28 @@ var checkObservability = async ({
2285
2468
  };
2286
2469
 
2287
2470
  // src/runner.ts
2288
- import { existsSync as existsSync9, readdirSync as readdirSync5, readFileSync as readFileSync12 } from "fs";
2289
- import { join as join12 } from "path";
2471
+ import { existsSync as existsSync11, readdirSync as readdirSync5, readFileSync as readFileSync14 } from "fs";
2472
+ import { join as join14, resolve as resolve2 } from "path";
2290
2473
  var TEST_FAILURES = "testFailures";
2291
2474
  var RUNS_A_RUNNER = /(?:^|[\s;&|(])(?:npx\s+|bunx\s+|pnpm\s+(?:exec\s+)?)?(?:vitest|bun\s+test)(?:\s|$)/;
2292
2475
  var RUNS_BUN_TEST = /(?:^|[\s;&|(])(?:bunx\s+)?bun\s+test(?:\s|$)/;
2293
- var WRITES_A_REPORT = /--reporter[= ]\S*json|--outputFile/i;
2294
- var finding9 = (subject, verdict, message) => ({
2476
+ var OUTPUT_FILE = /--outputFile[= ](\S+)/i;
2477
+ var A_SCRIPT_FILE = /\.(?:[cm]?[jt]sx?|sh|bash)$/;
2478
+ var wrapperNamedBy = (script, dir) => {
2479
+ for (const token of script.split(/[\s'"]+/)) {
2480
+ if (!A_SCRIPT_FILE.test(token)) continue;
2481
+ try {
2482
+ return { source: readFileSync14(resolve2(dir, token), "utf8"), where: token };
2483
+ } catch {
2484
+ continue;
2485
+ }
2486
+ }
2487
+ return void 0;
2488
+ };
2489
+ var asCommand = (source) => source.replaceAll(/['"`,[\]()]/gu, " ");
2490
+ var ASKS_FOR_A_REPORT = /--outputFile|--reporter[= ]\S*json/i;
2491
+ var PARSES_A_REPORT = /JSON\.parse|numFailedTests/;
2492
+ var finding10 = (subject, verdict, message) => ({
2295
2493
  check: "runner",
2296
2494
  message,
2297
2495
  subject,
@@ -2309,20 +2507,21 @@ var reportingCounters = (ratchet) => (ratchet?.counters ?? []).filter(
2309
2507
  (entry) => entry.counter === TEST_FAILURES && stringOf(entry.report) !== ""
2310
2508
  );
2311
2509
  var keyOf = (entry) => stringOf(entry.key) === "" ? TEST_FAILURES : stringOf(entry.key);
2510
+ var namesReport = (entry, path) => stringOf(entry.reportPath) === path || stringOf(entry.command).includes(path);
2312
2511
  var PROVES = /--prove\b/;
2313
2512
  var everythingRun2 = (root, workspaces) => {
2314
2513
  const scripts = workspaces.flatMap((one) => Object.values(one.manifest.scripts ?? {}));
2315
- const dir = join12(root, ".github/workflows");
2316
- const workflows = (existsSync9(dir) ? readdirSync5(dir) : []).filter((name) => name.endsWith(".yml") || name.endsWith(".yaml")).map((name) => {
2514
+ const dir = join14(root, ".github/workflows");
2515
+ const workflows = (existsSync11(dir) ? readdirSync5(dir) : []).filter((name) => name.endsWith(".yml") || name.endsWith(".yaml")).map((name) => {
2317
2516
  try {
2318
- return readFileSync12(join12(dir, name), "utf8");
2517
+ return readFileSync14(join14(dir, name), "utf8");
2319
2518
  } catch {
2320
2519
  return "";
2321
2520
  }
2322
2521
  });
2323
2522
  let tiers = {};
2324
2523
  try {
2325
- const config = JSON.parse(readFileSync12(join12(root, "geonosis.json"), "utf8"));
2524
+ const config = JSON.parse(readFileSync14(join14(root, "geonosis.json"), "utf8"));
2326
2525
  tiers = config.verify ?? {};
2327
2526
  } catch {
2328
2527
  }
@@ -2340,7 +2539,7 @@ var proveIsWired = ({
2340
2539
  const { run, tiers } = everythingRun2(root, workspaces);
2341
2540
  if (PROVES.test(run)) {
2342
2541
  return [
2343
- finding9(
2542
+ finding10(
2344
2543
  RATCHET_FILE,
2345
2544
  "OK",
2346
2545
  `${ratchet.counters.length} counter(s), and something here runs geonosis-ratchet --prove`
@@ -2349,7 +2548,7 @@ var proveIsWired = ({
2349
2548
  }
2350
2549
  const where = tiers.at(-1) ?? "full";
2351
2550
  return [
2352
- finding9(
2551
+ finding10(
2353
2552
  RATCHET_FILE,
2354
2553
  "WARN",
2355
2554
  `${ratchet.counters.length} counter(s) and nothing here ever runs geonosis-ratchet --prove \u2014 not a verify tier, not a script, not a workflow. A counter that has never been seen reading its own planted finding has not been shown to measure anything, and it reads exactly like a clean tree. Add \`geonosis-ratchet --prove\` to the "${where}" tier in geonosis.json, or to the workflow that runs it`
@@ -2367,44 +2566,61 @@ var checkRunner = ({
2367
2566
  if (script === "") return [];
2368
2567
  const subject = workspace.relative === "" ? "package.json" : `${workspace.relative}/package.json`;
2369
2568
  const said2 = (verdict, message) => [
2370
- finding9(subject, verdict, message)
2569
+ finding10(subject, verdict, message)
2371
2570
  ];
2372
- if (!RUNS_A_RUNNER.test(script)) {
2571
+ const wrapper = RUNS_A_RUNNER.test(script) ? void 0 : wrapperNamedBy(script, workspace.dir);
2572
+ const judged = wrapper === void 0 ? script : asCommand(wrapper.source);
2573
+ const read = wrapper === void 0 ? `"${script}"` : `"${script}", read one level into ${wrapper.where},`;
2574
+ if (!RUNS_A_RUNNER.test(judged)) {
2373
2575
  return said2(
2374
2576
  "OK",
2375
- `"${script}" runs neither vitest nor bun test \u2014 this check has nothing to say about it`
2577
+ `${read} runs neither vitest nor bun test \u2014 this check has nothing to say about it`
2376
2578
  );
2377
2579
  }
2378
2580
  if (testFilesUnder(workspace.dir).length === 0) {
2379
2581
  return said2(
2380
2582
  "SKIP",
2381
- `"${script}" runs a test runner and there is no test file under this workspace \u2014 nothing here for its exit code to be wrong about`
2583
+ `${read} runs a test runner and there is no test file under this workspace \u2014 nothing here for its exit code to be wrong about`
2382
2584
  );
2383
2585
  }
2384
- if (WRITES_A_REPORT.test(script)) {
2385
- return said2("OK", "the script asks the runner for its own JSON report, not for a status code");
2586
+ if (wrapper !== void 0 && ASKS_FOR_A_REPORT.test(wrapper.source) && PARSES_A_REPORT.test(wrapper.source)) {
2587
+ return said2(
2588
+ "OK",
2589
+ `${wrapper.where} spawns the runner, asks it for a JSON report and parses that report \u2014 the exit code is not the verdict here`
2590
+ );
2386
2591
  }
2592
+ const written = OUTPUT_FILE.exec(judged)?.[1];
2387
2593
  const counter = reading.find((entry) => covers(stringOf(entry.command), workspace, workspaces));
2388
2594
  if (counter !== void 0) {
2389
- return said2("OK", `read by the ratchet's "${keyOf(counter)}" counter in report mode`);
2595
+ const named2 = written !== void 0 && namesReport(counter, written);
2596
+ return said2(
2597
+ "OK",
2598
+ named2 ? `the script writes its JSON report to ${written} and the ratchet's "${keyOf(counter)}" counter reads that path` : `read by the ratchet's "${keyOf(counter)}" counter in report mode`
2599
+ );
2390
2600
  }
2391
- if (RUNS_BUN_TEST.test(script)) {
2601
+ if (RUNS_BUN_TEST.test(judged)) {
2392
2602
  return said2(
2393
2603
  "OK",
2394
- `"${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.`
2604
+ `${read} 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.`
2605
+ );
2606
+ }
2607
+ if (written !== void 0) {
2608
+ return said2(
2609
+ "WARN",
2610
+ `${read} writes the runner's JSON report to ${written} and nothing reads it \u2014 written, read by nothing, which leaves the exit code as the only verdict again. Give the ratchet a ${TEST_FAILURES} counter in report mode whose "reportPath" or command names ${written}.`
2395
2611
  );
2396
2612
  }
2397
2613
  return said2(
2398
2614
  "WARN",
2399
- `"${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.`
2615
+ `${read} 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 something reads.`
2400
2616
  );
2401
2617
  });
2402
2618
  return [...proveIsWired({ ratchet, root, workspaces }), ...perWorkspace2];
2403
2619
  };
2404
2620
 
2405
2621
  // src/doctor.ts
2406
- import { homedir as homedir2 } from "os";
2407
- import { join as join14 } from "path";
2622
+ import { homedir as homedir3 } from "os";
2623
+ import { join as join17 } from "path";
2408
2624
  import { resolveOxlint } from "@geonosis/lint-parity";
2409
2625
 
2410
2626
  // src/engine.ts
@@ -2432,16 +2648,32 @@ var checkEngines = async ({
2432
2648
  workspaces
2433
2649
  }) => {
2434
2650
  const plugin = await presumptionsOf(entry).catch(() => void 0);
2435
- if (plugin === void 0 || plugin.namespace === "" || Object.keys(plugin.presumed).length === 0) {
2651
+ if (plugin === void 0 || plugin.namespace === "" || Object.keys(plugin.presumed).length === 0 && Object.keys(plugin.floors).length === 0) {
2436
2652
  return [];
2437
2653
  }
2654
+ const pending = (rule) => {
2655
+ const floor = plugin.floors[rule.slice(plugin.namespace.length + 1)];
2656
+ if (floor === void 0) return [];
2657
+ return [
2658
+ {
2659
+ check: "exercised",
2660
+ message: `it names a pattern the kit does not ship yet (${floor.name}) \u2014 the rule is the requirement and there is nothing to install behind it, so a repo enabling it hand-writes the floor to satisfy the lint. Wait for the floor, or keep the pattern of your own and know that is what it is`,
2661
+ subject: `${config.relative} \u2192 ${rule}`,
2662
+ verdict: "WARN"
2663
+ }
2664
+ ];
2665
+ };
2438
2666
  const governed = governedBy(config.dir, workspaces);
2439
2667
  const declared = new Set(governed.flatMap((one) => [...declaredIn3(one.manifest)]));
2440
2668
  const asked = governed.map(labelOf3).join(", ");
2441
2669
  return enabledHere2(config, plugin.namespace).flatMap((rule) => {
2670
+ const floorless = pending(rule);
2442
2671
  const presumes = plugin.presumed[rule.slice(plugin.namespace.length + 1)];
2443
- if (presumes === void 0 || presumes.packages.some((name) => declared.has(name))) return [];
2672
+ if (presumes === void 0 || presumes.packages.some((name) => declared.has(name))) {
2673
+ return floorless;
2674
+ }
2444
2675
  return [
2676
+ ...floorless,
2445
2677
  {
2446
2678
  check: "exercised",
2447
2679
  message: `it presumes ${presumes.engine}, and none of the manifests this config governs (${asked}) declares ${presumes.packages.join(" or ")} \u2014 enabled here it has no possible finding, which reads exactly like a clean tree. Drop it from this config, or say which package brings the engine`,
@@ -2452,15 +2684,80 @@ var checkEngines = async ({
2452
2684
  });
2453
2685
  };
2454
2686
 
2687
+ // src/path-grants.ts
2688
+ import { readdirSync as readdirSync6 } from "fs";
2689
+ import { join as join15 } from "path";
2690
+ var OFF2 = /* @__PURE__ */ new Set([0, "0", "allow", "off", false]);
2691
+ var severityOf2 = (level) => Array.isArray(level) ? level[0] : level;
2692
+ var NEVER_WALKED3 = /* @__PURE__ */ new Set([".git", "coverage", "dist", "node_modules"]);
2693
+ var LINTABLE2 = /\.[cm]?[jt]sx?$/;
2694
+ var filesUnder2 = (root, at = "") => {
2695
+ let entries;
2696
+ try {
2697
+ entries = readdirSync6(join15(root, at), { withFileTypes: true });
2698
+ } catch {
2699
+ return [];
2700
+ }
2701
+ return entries.flatMap((entry) => {
2702
+ const here = at === "" ? entry.name : `${at}/${entry.name}`;
2703
+ if (entry.isDirectory()) {
2704
+ return NEVER_WALKED3.has(entry.name) ? [] : filesUnder2(root, here);
2705
+ }
2706
+ return LINTABLE2.test(entry.name) ? [here] : [];
2707
+ });
2708
+ };
2709
+ var stringsOf2 = (value) => Array.isArray(value) ? value.filter((one) => typeof one === "string") : [];
2710
+ var bagOf = (level) => {
2711
+ if (!Array.isArray(level)) return {};
2712
+ const [, options] = level;
2713
+ return typeof options === "object" && options !== null && !Array.isArray(options) ? options : {};
2714
+ };
2715
+ var matches = (patterns, file) => patterns.some((pattern) => {
2716
+ if (file.includes(pattern)) return true;
2717
+ try {
2718
+ return new RegExp(pattern).test(file);
2719
+ } catch {
2720
+ return false;
2721
+ }
2722
+ });
2723
+ var checkPathGrants = ({
2724
+ config,
2725
+ grants,
2726
+ root
2727
+ }) => {
2728
+ const declared = Object.entries(grants);
2729
+ if (declared.length === 0) return [];
2730
+ const files = filesUnder2(root);
2731
+ const found = [];
2732
+ for (const [id, grant] of declared) {
2733
+ for (const layer of layersOf(config, id)) {
2734
+ if (OFF2.has(severityOf2(layer.level))) continue;
2735
+ const options = bagOf(layer.level);
2736
+ const patterns = stringsOf2(options[grant.paths]);
2737
+ if (patterns.length === 0) continue;
2738
+ const enumerated = stringsOf2(options[grant.modules]);
2739
+ const exempted = files.filter((file) => matches(patterns, file));
2740
+ if (exempted.length <= enumerated.length) continue;
2741
+ found.push({
2742
+ check: "exercised",
2743
+ message: `${id} exempts ${exempted.length} file${exempted.length === 1 ? "" : "s"} in this tree by \`${grant.paths}\` (${patterns.join(", ")}) and enumerates ${enumerated.length} in \`${grant.modules}\`: a pattern grants its exemption to wherever it stops, so nothing counts what accumulates on the far side. Name them \u2014 ${exempted.slice(0, 5).join(", ")}${exempted.length > 5 ? ", \u2026" : ""} \u2014 in \`${grant.modules}\`.`,
2744
+ subject: config.relative,
2745
+ verdict: "WARN"
2746
+ });
2747
+ }
2748
+ }
2749
+ return found;
2750
+ };
2751
+
2455
2752
  // src/rails.ts
2456
- import { existsSync as existsSync10, readFileSync as readFileSync13 } from "fs";
2457
- import { join as join13 } from "path";
2753
+ import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
2754
+ import { join as join16 } from "path";
2458
2755
  var GEONOSIS2 = "geonosis.json";
2459
2756
  var PROJECT = ".claude/settings.json";
2460
2757
  var LOCAL = ".claude/settings.local.json";
2461
2758
  var RUN_RECORD = ".geonosis/rails-run.json";
2462
2759
  var managedSettingsPath = (platform = process.platform) => platform === "darwin" ? "/Library/Application Support/ClaudeCode/managed-settings.json" : "/etc/claude-code/managed-settings.json";
2463
- var finding10 = (subject, verdict, message) => ({
2760
+ var finding11 = (subject, verdict, message) => ({
2464
2761
  check: "rails",
2465
2762
  message,
2466
2763
  subject,
@@ -2480,10 +2777,10 @@ var networkIn = (parsed) => {
2480
2777
  };
2481
2778
  };
2482
2779
  var sourceAt = (path, label) => {
2483
- if (!existsSync10(path)) return { network: void 0, path: label, unreadable: false };
2780
+ if (!existsSync12(path)) return { network: void 0, path: label, unreadable: false };
2484
2781
  try {
2485
2782
  return {
2486
- network: networkIn(JSON.parse(readFileSync13(path, "utf8"))),
2783
+ network: networkIn(JSON.parse(readFileSync15(path, "utf8"))),
2487
2784
  path: label,
2488
2785
  unreadable: false
2489
2786
  };
@@ -2492,11 +2789,11 @@ var sourceAt = (path, label) => {
2492
2789
  }
2493
2790
  };
2494
2791
  var declaredEgress = (root) => {
2495
- const at = join13(root, GEONOSIS2);
2496
- if (!existsSync10(at)) return void 0;
2792
+ const at = join16(root, GEONOSIS2);
2793
+ if (!existsSync12(at)) return void 0;
2497
2794
  let parsed;
2498
2795
  try {
2499
- parsed = JSON.parse(readFileSync13(at, "utf8"));
2796
+ parsed = JSON.parse(readFileSync15(at, "utf8"));
2500
2797
  } catch {
2501
2798
  return void 0;
2502
2799
  }
@@ -2508,10 +2805,10 @@ var declaredEgress = (root) => {
2508
2805
  };
2509
2806
  };
2510
2807
  var deniedEgress = (root) => {
2511
- const at = join13(root, RUN_RECORD);
2512
- if (!existsSync10(at)) {
2808
+ const at = join16(root, RUN_RECORD);
2809
+ if (!existsSync12(at)) {
2513
2810
  return [
2514
- finding10(
2811
+ finding11(
2515
2812
  "deniedEgress",
2516
2813
  "SKIP",
2517
2814
  `there is no ${RUN_RECORD} here, so no run has been recorded for this to be a gate over`
@@ -2520,10 +2817,10 @@ var deniedEgress = (root) => {
2520
2817
  }
2521
2818
  let parsed;
2522
2819
  try {
2523
- parsed = JSON.parse(readFileSync13(at, "utf8"));
2820
+ parsed = JSON.parse(readFileSync15(at, "utf8"));
2524
2821
  } catch (error) {
2525
2822
  return [
2526
- finding10(
2823
+ finding11(
2527
2824
  "deniedEgress",
2528
2825
  "FAIL",
2529
2826
  `${RUN_RECORD} is not readable JSON (${error.message}) \u2014 a record nobody can read is not a record of zero denied attempts`
@@ -2532,11 +2829,11 @@ var deniedEgress = (root) => {
2532
2829
  }
2533
2830
  const denied = isRecord3(parsed) && Array.isArray(parsed["deniedEgress"]) ? parsed["deniedEgress"] : [];
2534
2831
  if (denied.length === 0) {
2535
- return [finding10("deniedEgress", "OK", "no run recorded a denied egress attempt")];
2832
+ return [finding11("deniedEgress", "OK", "no run recorded a denied egress attempt")];
2536
2833
  }
2537
2834
  const hosts = denied.map((one) => isRecord3(one) && typeof one["host"] === "string" ? one["host"] : "(unnamed)").join(", ");
2538
2835
  return [
2539
- finding10(
2836
+ finding11(
2540
2837
  "deniedEgress",
2541
2838
  "FAIL",
2542
2839
  `${denied.length} denied egress attempt(s) in this run's record \u2014 ${hosts}. This is a gate at ZERO and never a counter to hold flat: a run that reached for a host this repo does not allow is a run somebody reads, whatever the number was yesterday`
@@ -2551,7 +2848,7 @@ var checkRails = ({
2551
2848
  const declared = declaredEgress(root);
2552
2849
  if (declared === void 0 || declared.allow.length === 0) {
2553
2850
  return [
2554
- finding10(
2851
+ finding11(
2555
2852
  `${GEONOSIS2} \u2192 rails.egress`,
2556
2853
  "SKIP",
2557
2854
  "this repo declares no egress allowlist, so there is no rendered setting for this to read back"
@@ -2561,11 +2858,11 @@ var checkRails = ({
2561
2858
  const sources = [
2562
2859
  sourceAt(managedSettings, managedSettings),
2563
2860
  sourceAt(userSettings, userSettings),
2564
- sourceAt(join13(root, PROJECT), PROJECT),
2565
- sourceAt(join13(root, LOCAL), LOCAL)
2861
+ sourceAt(join16(root, PROJECT), PROJECT),
2862
+ sourceAt(join16(root, LOCAL), LOCAL)
2566
2863
  ];
2567
2864
  const unreadable = sources.filter((one) => one.unreadable).map(
2568
- (one) => finding10(
2865
+ (one) => finding11(
2569
2866
  one.path,
2570
2867
  "FAIL",
2571
2868
  "is not readable JSON, and Claude Code SILENTLY ignores a settings file that fails validation \u2014 every setting rendered into this file loads as nothing, with no error anywhere. Fix the file, then render the allowlist again"
@@ -2580,7 +2877,7 @@ var checkRails = ({
2580
2877
  return [
2581
2878
  ...unreadable,
2582
2879
  ...declared.allow.map(
2583
- (domain) => effective.has(domain) ? finding10(domain, "OK", "declared, and in the settings that actually load") : finding10(
2880
+ (domain) => effective.has(domain) ? finding11(domain, "OK", "declared, and in the settings that actually load") : finding11(
2584
2881
  domain,
2585
2882
  "FAIL",
2586
2883
  `declared in ${GEONOSIS2} \u2192 rails.egress and absent from every settings file that loads, so the run has no allowance for it.${why}`
@@ -2660,7 +2957,7 @@ var exercisedOf = ({
2660
2957
  workspaces
2661
2958
  }) => Promise.all(
2662
2959
  configs.flatMap(
2663
- (config) => config.jsPlugins.filter((name) => name.startsWith(SCOPE)).map(async (specifier) => {
2960
+ (config) => config.jsPlugins.filter((name) => name.startsWith(SCOPE2)).map(async (specifier) => {
2664
2961
  let corpus;
2665
2962
  let entry;
2666
2963
  try {
@@ -2677,6 +2974,7 @@ var exercisedOf = ({
2677
2974
  ];
2678
2975
  }
2679
2976
  const own = repoCorpus !== void 0 && config.relative === CONFIG_FILE ? repoCorpus : void 0;
2977
+ const grants = await grantsOf(entry).catch(() => ({}));
2680
2978
  const [reach, engines] = await Promise.all([
2681
2979
  checkExercised({
2682
2980
  config,
@@ -2690,14 +2988,14 @@ var exercisedOf = ({
2690
2988
  // inside it — one is about the corpus, the other about this repo's own manifests.
2691
2989
  checkEngines({ config, entry, workspaces })
2692
2990
  ]);
2693
- return [reach, ...engines];
2991
+ return [reach, ...engines, ...checkPathGrants({ config, grants, root })];
2694
2992
  })
2695
2993
  )
2696
2994
  );
2697
2995
  var requiredOptionsOf = async (configs) => {
2698
2996
  const found = await Promise.all(
2699
2997
  configs.flatMap(
2700
- (config) => config.jsPlugins.filter((name) => name.startsWith(SCOPE)).map(async (specifier) => {
2998
+ (config) => config.jsPlugins.filter((name) => name.startsWith(SCOPE2)).map(async (specifier) => {
2701
2999
  let entry;
2702
3000
  try {
2703
3001
  entry = resolveFrom(config.dir, specifier);
@@ -2733,6 +3031,7 @@ var countsOf = (findings) => findings.reduce((counts, one) => ({ ...counts, [one
2733
3031
  });
2734
3032
  var runDoctor = async ({
2735
3033
  baseline,
3034
+ home = homedir3(),
2736
3035
  only,
2737
3036
  oxlint,
2738
3037
  root,
@@ -2750,7 +3049,7 @@ var runDoctor = async ({
2750
3049
  [
2751
3050
  "loaded",
2752
3051
  async () => [
2753
- ...await checkLoaded({ configs, root, workspaces }),
3052
+ ...await checkLoaded({ configs, home, root, workspaces }),
2754
3053
  ...await requiredOptionsOf(configs)
2755
3054
  ]
2756
3055
  ],
@@ -2784,7 +3083,8 @@ var runDoctor = async ({
2784
3083
  ["observability", () => checkObservability({ now: Date.now(), root })],
2785
3084
  ["drift", () => checkDrift({ root, workspaces })],
2786
3085
  ["deployed", () => checkDeployed({ root })],
2787
- ["rails", () => checkRails({ root, userSettings: join14(homedir2(), USER_SETTINGS) })]
3086
+ ["rails", () => checkRails({ root, userSettings: join17(home, USER_SETTINGS) })],
3087
+ ["exams", () => checkExams({ root, workspaces })]
2788
3088
  ];
2789
3089
  const collected = [];
2790
3090
  const ran = [];
@@ -2810,6 +3110,7 @@ var ABOUT = {
2810
3110
  deployed: "what the pipeline reported deploying is what the tree declares",
2811
3111
  drift: "the gates that were set up and are no longer running",
2812
3112
  envelope: "every gate read as many things as it was handed",
3113
+ exams: "every adopted floor has a test file that runs its exam",
2813
3114
  exercised: "every enabled rule fires on at least one corpus file",
2814
3115
  group: "the packages published on one version resolve to one version",
2815
3116
  loaded: "the plugin oxlint would load is the one the manifest pins",
@@ -2878,7 +3179,12 @@ export {
2878
3179
  ENVELOPES_DIR,
2879
3180
  NO_ENVELOPES,
2880
3181
  checkEnvelopes,
2881
- SCOPE,
3182
+ checkExams,
3183
+ INSTALLED_PLUGINS,
3184
+ PLUGIN_NAME,
3185
+ installedPluginVersions,
3186
+ checkClaudePlugin,
3187
+ SCOPE2 as SCOPE,
2882
3188
  satisfies,
2883
3189
  declaredFor,
2884
3190
  checkLoaded,
@@ -4,15 +4,15 @@ import {
4
4
  formatDoctor,
5
5
  formatJson,
6
6
  runDoctor
7
- } from "./chunk-R2R2OYH3.js";
7
+ } from "./chunk-GWVSC7GZ.js";
8
8
 
9
9
  // src/doctor-cli.ts
10
10
  import { fstatSync, statSync } from "fs";
11
- import { resolve } from "path";
11
+ import { resolve as resolve2 } from "path";
12
12
 
13
13
  // src/envelope-write.ts
14
14
  import { mkdirSync, readFileSync, writeFileSync } from "fs";
15
- import { dirname, join } from "path";
15
+ import { dirname, join, resolve } from "path";
16
16
  import { fileURLToPath } from "url";
17
17
  var envelopePath = (root, tool) => join(root, ENVELOPES_DIR, `${tool}.json`);
18
18
  var UnbalancedEnvelope = class extends Error {
@@ -23,6 +23,14 @@ var UnbalancedEnvelope = class extends Error {
23
23
  };
24
24
  var isCount = (value) => Number.isSafeInteger(value) && value >= 0;
25
25
  var unbalancedMessage = (envelope, next) => `${envelope.tool}: considered ${envelope.considered} but accounts for ${envelope.read + envelope.refused.length + envelope.excused.length} \u2014 ${envelope.read} read + ${envelope.refused.length} refused + ${envelope.excused.length} excused. A run that has lost count of its own inputs cannot say what it measured, so no verdict was rendered and no envelope was written. Next: ${next}`;
26
+ var FORBIDDEN_ROOT = "GEONOSIS_ENVELOPES_FORBIDDEN_ROOT";
27
+ var refuseForbiddenRoot = (root) => {
28
+ const forbidden = process.env[FORBIDDEN_ROOT];
29
+ if (forbidden === void 0 || resolve(forbidden) !== resolve(root)) return;
30
+ throw new UnbalancedEnvelope(
31
+ `${root} is off limits to envelope writers in this process (${FORBIDDEN_ROOT}) \u2014 a run that writes one into a shared root races every other run reading it, and leaves a file the next one takes for real. Point this at a scratch root of its own: tooling/scratch-dir.ts.`
32
+ );
33
+ };
26
34
  var writeEnvelope = ({
27
35
  envelope,
28
36
  next,
@@ -46,6 +54,7 @@ var writeEnvelope = ({
46
54
  if (envelope.considered !== envelope.read + envelope.refused.length + envelope.excused.length) {
47
55
  throw new UnbalancedEnvelope(unbalancedMessage(envelope, next));
48
56
  }
57
+ refuseForbiddenRoot(root);
49
58
  const at = envelopePath(root, envelope.tool);
50
59
  mkdirSync(dirname(at), { recursive: true });
51
60
  writeFileSync(at, `${JSON.stringify(envelope, void 0, 2)}
@@ -120,6 +129,12 @@ var ABOUT = {
120
129
  parity run over a tree the second config ignored. Each published a numerator and no
121
130
  denominator. Reads the files and imports nothing of the tools that wrote them; no
122
131
  envelopes at all is a SKIP with the sentence.`,
132
+ exams: `Every @geonosis/* floor a manifest here declares, against the test files in this tree: a
133
+ floor whose exam nothing imports is a floor whose break the tier cannot see, and a
134
+ bump that broke it would read exactly like a green run. The exams are read off the
135
+ installed floor's own declarations \u2014 every export ending in Conformance \u2014 never off a
136
+ list held here. WARNs, because a floor adopted last week and one whose exam somebody
137
+ deleted look the same from outside.`,
123
138
  exercised: `Every rule a config enables, against the corpus the loaded plugin ships. A rule at
124
139
  "error" that can never fire is indistinguishable from a clean tree. The line says what
125
140
  its number is evidence ABOUT \u2014 the corpus and the probes, never this tree \u2014 and a rule
@@ -181,7 +196,7 @@ plugin-route-namespaced on its package root, while the shipped corpus says acme
181
196
 
182
197
  --print-config-shape every file this reads, and the keys it reads out of each
183
198
 
184
- Every run writes .geonosis/envelopes/doctor.json: the ten checks CONSIDERED against the ones this
199
+ Every run writes .geonosis/envelopes/doctor.json: the eleven checks CONSIDERED against the ones this
185
200
  run read, with the rest excused by name, so a --only run can never read as a verdict about the whole
186
201
  tree. The envelope check SKIPs that one file \u2014 this run is the one writing it.
187
202
 
@@ -225,8 +240,8 @@ var parseDoctorArgs = (argv, cwd) => {
225
240
  ...baseline === void 0 ? {} : { baseline },
226
241
  json,
227
242
  ...only === void 0 || only.length === 0 ? {} : { only },
228
- ...read["--oxlint"] === void 0 ? {} : { oxlint: resolve(cwd, read["--oxlint"]) },
229
- root: resolve(cwd, read["--root"] ?? cwd),
243
+ ...read["--oxlint"] === void 0 ? {} : { oxlint: resolve2(cwd, read["--oxlint"]) },
244
+ root: resolve2(cwd, read["--root"] ?? cwd),
230
245
  strict
231
246
  };
232
247
  };
package/dist/index.d.ts CHANGED
@@ -11,7 +11,7 @@ type Overrides = {
11
11
  * question one layer out — whether what was reported is what happened; `rails` asks it of the
12
12
  * settings an unattended run is bounded by, where a rendered file is not a loaded setting.
13
13
  */
14
- declare const CHECKS: readonly ["loaded", "group", "exercised", "baseline", "runner", "envelope", "drift", "observability", "deployed", "rails"];
14
+ declare const CHECKS: readonly ["loaded", "group", "exercised", "baseline", "runner", "envelope", "drift", "observability", "deployed", "rails", "exams"];
15
15
  type CheckName = (typeof CHECKS)[number];
16
16
  /**
17
17
  * `SKIP` is a first-class answer and is printed like any other: a check whose line is missing reads
@@ -66,6 +66,8 @@ type RatchetConfig = {
66
66
  counters: Record<string, unknown>[];
67
67
  };
68
68
  type DoctorOptions = {
69
+ /** Where `~/.claude` lives — the plugin registry and the user settings the doctor reads; the home directory unless a caller says otherwise. */
70
+ home?: string;
69
71
  /**
70
72
  * Present only when a baseline comparison was asked for, because making one is the one part of
71
73
  * this tool that runs git — and the release check runs the doctor inside repos other sessions
@@ -138,7 +140,7 @@ declare const discoverConfigs: (root: string) => DiscoveredConfig[];
138
140
  declare const discoverWorkspaces: (root: string) => Workspace[];
139
141
  declare const readRatchet: (root: string) => RatchetConfig | undefined;
140
142
 
141
- declare const runDoctor: ({ baseline, only, oxlint, root, strict, }: DoctorOptions) => Promise<DoctorReport>;
143
+ declare const runDoctor: ({ baseline, home, only, oxlint, root, strict, }: DoctorOptions) => Promise<DoctorReport>;
142
144
 
143
145
  /** The file `create-geonosis --domains` writes, held against the scaffold's own name by a test. */
144
146
  declare const COMPOSITION_ROOT = "src/platform.ts";
@@ -265,6 +267,39 @@ declare const checkGroup: ({ groups, root, workspaces, }: {
265
267
  workspaces: Workspace[];
266
268
  }) => Finding[];
267
269
 
270
+ /**
271
+ * Where Claude Code records what it has installed.
272
+ *
273
+ * MEASURED 2026-09-02 (`~/.claude/plugins/installed_plugins.json`, `version: 2`): a map keyed
274
+ * `<plugin>@<marketplace>`, each value a list of installations carrying `version`, `installPath`
275
+ * and the commit it came from. This is the file `claude plugin list` answers from.
276
+ */
277
+ declare const INSTALLED_PLUGINS = ".claude/plugins/installed_plugins.json";
278
+ /** The plugin this kit publishes. Its own name, the way `FIXED_GROUP` is its own group. */
279
+ declare const PLUGIN_NAME = "geonosis";
280
+ /**
281
+ * Every version of one plugin this machine has installed, in the order the record lists them.
282
+ *
283
+ * A name can be installed from more than one marketplace and at more than one scope, so this
284
+ * answers with a list rather than picking one: a session loads what it loads, and a doctor that
285
+ * chose for the reader would be reporting about an installation nobody has.
286
+ */
287
+ declare const installedPluginVersions: (home: string, name: string) => string[];
288
+ /**
289
+ * #133: a consumer's npm bump does not move the CLAUDE PLUGIN.
290
+ *
291
+ * The hooks, the agents and the skills live in the plugin, and it updates on `claude plugin update`
292
+ * and never on an install — so a repo can be on this release's packages while every session in it
293
+ * runs the hooks of the one before, and a refusal added this release cannot fire. That is the
294
+ * declared-versus-loaded question one surface out, and it is this check's to ask.
295
+ */
296
+ declare const checkClaudePlugin: ({ home, name, train, }: {
297
+ home?: string;
298
+ name?: string;
299
+ /** The version the package train is on here, or nothing when this tree declares none. */
300
+ train: string | undefined;
301
+ }) => Finding[];
302
+
268
303
  /** The scope this tool asks about. A config's other plugins are somebody else's release to check. */
269
304
  declare const SCOPE = "@geonosis/";
270
305
  /**
@@ -286,8 +321,9 @@ declare const declaredFor: ({ dir, specifier, workspaces, }: {
286
321
  at: string;
287
322
  spec: string;
288
323
  } | undefined;
289
- declare const checkLoaded: ({ configs, root, workspaces, }: {
324
+ declare const checkLoaded: ({ configs, home, root, workspaces, }: {
290
325
  configs: DiscoveredConfig[];
326
+ home?: string;
291
327
  root: string;
292
328
  workspaces: Workspace[];
293
329
  }) => Promise<Finding[]>;
@@ -353,6 +389,23 @@ declare const corpusOfPlugin: (from: string, specifier: string) => string;
353
389
  */
354
390
  declare const relativeToRoot: (root: string, path: string) => string;
355
391
 
392
+ /**
393
+ * Whether an adopted floor is exercised by a test file at all.
394
+ *
395
+ * A verify tier declares COMMANDS, not globs, so "the exam runs in the fast tier" is not a claim
396
+ * anything can check. What is checkable is whether a test file importing the exam exists: a floor a
397
+ * consumer adopted and never exams is a floor whose break their gate cannot see, and a floor bump
398
+ * that broke it would read exactly like a green tier (#293).
399
+ *
400
+ * It WARNs rather than FAILs. The check cannot tell a floor adopted last week from one whose exam
401
+ * somebody deleted, and a FAIL on every fresh install is the shape that teaches a reader to skim
402
+ * the section — the lesson the `runner` check already learned about warning the wrong runner.
403
+ */
404
+ declare const checkExams: ({ root, workspaces, }: {
405
+ root: string;
406
+ workspaces: Workspace[];
407
+ }) => Finding[];
408
+
356
409
  /**
357
410
  * Whether a test runner's exit code is a verdict — asked of the runner the script actually names,
358
411
  * not of runners in general.
@@ -370,4 +423,4 @@ declare const checkRunner: ({ ratchet, root, workspaces, }: {
370
423
  workspaces: Workspace[];
371
424
  }) => Finding[];
372
425
 
373
- export { CHECKS, COMPOSITION_ROOT, CONFIG_FILE, type CheckName, DEPLOYED_FILE, type DiscoveredConfig, DoctorError, type DoctorOptions, type DoctorReport, ENVELOPES_DIR, FIXED_GROUP, FLOOR_PACKAGES, type Finding, GEONOSIS_FILE, type HeldTogether, KIT_GROUP, 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, checkGroup, checkLoaded, checkObservability, checkRunner, corpusOfPlugin, declaredFor, declaredGroupsOf, defaultRef, discoverConfigs, discoverWorkspaces, enabledRulesOf, formatDoctor, formatJson, packageDirOf, pluginVersionOf, readConfig, readRatchet, relativePath, relativeToRoot, repoCorpusOf, resolveFrom, runDoctor, satisfies };
426
+ export { CHECKS, COMPOSITION_ROOT, CONFIG_FILE, type CheckName, DEPLOYED_FILE, type DiscoveredConfig, DoctorError, type DoctorOptions, type DoctorReport, ENVELOPES_DIR, FIXED_GROUP, FLOOR_PACKAGES, type Finding, GEONOSIS_FILE, type HeldTogether, INSTALLED_PLUGINS, KIT_GROUP, type LastEventRecord, MANIFEST_FILE, type Manifest, NOT_WRITTEN, NO_ENVELOPES, type ObservabilityConfig, PLUGIN_NAME, RATCHET_FILE, READERS, type RatchetConfig, SCOPE, type Verdict, type Workspace, checkBaseline, checkClaudePlugin, checkDeployed, checkDrift, checkEnvelopes, checkExams, checkExercised, checkGroup, checkLoaded, checkObservability, checkRunner, corpusOfPlugin, declaredFor, declaredGroupsOf, defaultRef, discoverConfigs, discoverWorkspaces, enabledRulesOf, formatDoctor, formatJson, installedPluginVersions, packageDirOf, pluginVersionOf, readConfig, readRatchet, relativePath, relativeToRoot, repoCorpusOf, resolveFrom, runDoctor, satisfies };
package/dist/index.js CHANGED
@@ -8,17 +8,21 @@ import {
8
8
  FIXED_GROUP,
9
9
  FLOOR_PACKAGES,
10
10
  GEONOSIS_FILE,
11
+ INSTALLED_PLUGINS,
11
12
  KIT_GROUP,
12
13
  MANIFEST_FILE,
13
14
  NOT_WRITTEN,
14
15
  NO_ENVELOPES,
16
+ PLUGIN_NAME,
15
17
  RATCHET_FILE,
16
18
  READERS,
17
19
  SCOPE,
18
20
  checkBaseline,
21
+ checkClaudePlugin,
19
22
  checkDeployed,
20
23
  checkDrift,
21
24
  checkEnvelopes,
25
+ checkExams,
22
26
  checkExercised,
23
27
  checkGroup,
24
28
  checkLoaded,
@@ -33,6 +37,7 @@ import {
33
37
  enabledRulesOf,
34
38
  formatDoctor,
35
39
  formatJson,
40
+ installedPluginVersions,
36
41
  packageDirOf,
37
42
  pluginVersionOf,
38
43
  readConfig,
@@ -43,7 +48,7 @@ import {
43
48
  resolveFrom,
44
49
  runDoctor,
45
50
  satisfies
46
- } from "./chunk-R2R2OYH3.js";
51
+ } from "./chunk-GWVSC7GZ.js";
47
52
  export {
48
53
  CHECKS,
49
54
  COMPOSITION_ROOT,
@@ -54,17 +59,21 @@ export {
54
59
  FIXED_GROUP,
55
60
  FLOOR_PACKAGES,
56
61
  GEONOSIS_FILE,
62
+ INSTALLED_PLUGINS,
57
63
  KIT_GROUP,
58
64
  MANIFEST_FILE,
59
65
  NOT_WRITTEN,
60
66
  NO_ENVELOPES,
67
+ PLUGIN_NAME,
61
68
  RATCHET_FILE,
62
69
  READERS,
63
70
  SCOPE,
64
71
  checkBaseline,
72
+ checkClaudePlugin,
65
73
  checkDeployed,
66
74
  checkDrift,
67
75
  checkEnvelopes,
76
+ checkExams,
68
77
  checkExercised,
69
78
  checkGroup,
70
79
  checkLoaded,
@@ -79,6 +88,7 @@ export {
79
88
  enabledRulesOf,
80
89
  formatDoctor,
81
90
  formatJson,
91
+ installedPluginVersions,
82
92
  packageDirOf,
83
93
  pluginVersionOf,
84
94
  readConfig,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geonosis/doctor",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
4
4
  "types": "./dist/index.d.ts",
5
5
  "description": "The adoption doctor — declared ≠ loaded, enabled ≠ exercised, a baseline that grew, a runner whose exit code is the only verdict.",
6
6
  "keywords": [
@@ -35,10 +35,10 @@
35
35
  "dist"
36
36
  ],
37
37
  "dependencies": {
38
- "@geonosis/lint-parity": "2.1.0"
38
+ "@geonosis/lint-parity": "2.3.0"
39
39
  },
40
40
  "devDependencies": {
41
- "@geonosis/ratchet": "2.1.0"
41
+ "@geonosis/ratchet": "2.3.0"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "oxlint": ">=1.77"