@geonosis/doctor 2.2.0 → 2.3.1

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,6 +428,16 @@ 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;
@@ -1344,7 +1359,7 @@ var blocks = (root, config, readers, workspaces) => {
1344
1359
  finding4(
1345
1360
  name,
1346
1361
  "WARN",
1347
- `${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`
1348
1363
  )
1349
1364
  ];
1350
1365
  }
@@ -1907,18 +1922,116 @@ var checkEnvelopes = ({ root }) => {
1907
1922
  });
1908
1923
  };
1909
1924
 
1910
- // src/claude-plugin.ts
1925
+ // src/exams.ts
1911
1926
  import { existsSync as existsSync9, readFileSync as readFileSync10 } from "fs";
1927
+ import { dirname as dirname3, join as join10 } from "path";
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";
1912
2025
  import { homedir as homedir2 } from "os";
1913
- import { join as join10 } from "path";
2026
+ import { join as join11 } from "path";
1914
2027
  var INSTALLED_PLUGINS = ".claude/plugins/installed_plugins.json";
1915
2028
  var PLUGIN_NAME = "geonosis";
1916
2029
  var installedPluginVersions = (home, name) => {
1917
- const at = join10(home, INSTALLED_PLUGINS);
1918
- if (!existsSync9(at)) return [];
2030
+ const at = join11(home, INSTALLED_PLUGINS);
2031
+ if (!existsSync10(at)) return [];
1919
2032
  let record;
1920
2033
  try {
1921
- record = JSON.parse(readFileSync10(at, "utf8"));
2034
+ record = JSON.parse(readFileSync11(at, "utf8"));
1922
2035
  } catch {
1923
2036
  return [];
1924
2037
  }
@@ -1954,9 +2067,9 @@ var checkClaudePlugin = ({
1954
2067
  };
1955
2068
 
1956
2069
  // src/loaded.ts
1957
- import { readFileSync as readFileSync11 } from "fs";
1958
- import { join as join11, sep as sep3 } from "path";
1959
- var SCOPE = "@geonosis/";
2070
+ import { readFileSync as readFileSync12 } from "fs";
2071
+ import { join as join12, sep as sep3 } from "path";
2072
+ var SCOPE2 = "@geonosis/";
1960
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.";
1961
2074
  var BLOCKS = [
1962
2075
  "dependencies",
@@ -2026,7 +2139,7 @@ var declaredFor = ({
2026
2139
  }
2027
2140
  return void 0;
2028
2141
  };
2029
- var finding7 = (subject, verdict, message) => ({
2142
+ var finding8 = (subject, verdict, message) => ({
2030
2143
  check: "loaded",
2031
2144
  message,
2032
2145
  subject,
@@ -2043,7 +2156,7 @@ var oneConfig = async ({
2043
2156
  specifier,
2044
2157
  workspaces
2045
2158
  }) => {
2046
- const said2 = (verdict, message) => finding7(config.relative, verdict, `${specifier}: ${message}`);
2159
+ const said2 = (verdict, message) => finding8(config.relative, verdict, `${specifier}: ${message}`);
2047
2160
  let loaded;
2048
2161
  try {
2049
2162
  loaded = await versionAt(resolveFrom(config.dir, specifier), specifier, root);
@@ -2095,10 +2208,10 @@ var copiesOf = async ({
2095
2208
  found.set(at, { from: [labelOf2(workspace)], version: await pluginVersionOf(entry) });
2096
2209
  }
2097
2210
  if (found.size === 0) {
2098
- 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");
2099
2212
  }
2100
2213
  const listed = [...found.entries()].map(([at, one]) => `${at} ${one.version} (${one.from.join(", ")})`).join("; ");
2101
- 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(
2102
2215
  specifier,
2103
2216
  "WARN",
2104
2217
  `${found.size} copies \u2014 ${listed}. Which one oxlint runs depends on which directory its config sits in.`
@@ -2108,7 +2221,7 @@ var shipperOf = (root, bin) => {
2108
2221
  const name = bin === "geonosis" ? "@geonosis/cli" : `@geonosis/${bin.replace(/^geonosis-/, "")}`;
2109
2222
  try {
2110
2223
  const manifest = JSON.parse(
2111
- readFileSync11(join11(packageDirOf(resolveFrom(root, name), name), "package.json"), "utf8")
2224
+ readFileSync12(join12(packageDirOf(resolveFrom(root, name), name), "package.json"), "utf8")
2112
2225
  );
2113
2226
  const declared = manifest.bin;
2114
2227
  return typeof declared === "object" && !Object.hasOwn(declared, bin) ? "" : name;
@@ -2123,14 +2236,14 @@ var knipNote = (root, bin) => {
2123
2236
  var gitHooks = (root) => {
2124
2237
  const lines = hookLines(root);
2125
2238
  if (lines.length === 0) {
2126
- 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")];
2127
2240
  }
2128
2241
  const files = [...new Set(lines.map((one) => one.at))].toSorted();
2129
2242
  return files.flatMap((at) => {
2130
2243
  const here = lines.filter((one) => one.at === at);
2131
2244
  const named2 = here.filter((one) => NAMES_A_BIN.test(one.line));
2132
2245
  if (named2.length === 0) {
2133
- 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")];
2134
2247
  }
2135
2248
  const wrong = named2.flatMap((one) => {
2136
2249
  const found = VERSION_MANAGED.exec(one.line)?.groups;
@@ -2138,7 +2251,7 @@ var gitHooks = (root) => {
2138
2251
  });
2139
2252
  if (wrong.length === 0) {
2140
2253
  return [
2141
- finding7(
2254
+ finding8(
2142
2255
  at,
2143
2256
  "OK",
2144
2257
  `${named2.length} geonosis bin(s) here, every one called directly rather than through a runner`
@@ -2146,7 +2259,7 @@ var gitHooks = (root) => {
2146
2259
  ];
2147
2260
  }
2148
2261
  return wrong.map(
2149
- ({ bin, runner }) => finding7(
2262
+ ({ bin, runner }) => finding8(
2150
2263
  at,
2151
2264
  "FAIL",
2152
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)}`
@@ -2164,10 +2277,10 @@ var checkLoaded = async ({
2164
2277
  const specifiers = /* @__PURE__ */ new Set();
2165
2278
  for (const config of configs) {
2166
2279
  if (config.error !== void 0) {
2167
- findings.push(finding7(config.relative, "FAIL", config.error));
2280
+ findings.push(finding8(config.relative, "FAIL", config.error));
2168
2281
  continue;
2169
2282
  }
2170
- for (const specifier of config.jsPlugins.filter((name) => name.startsWith(SCOPE))) {
2283
+ for (const specifier of config.jsPlugins.filter((name) => name.startsWith(SCOPE2))) {
2171
2284
  specifiers.add(specifier);
2172
2285
  findings.push(await oneConfig({ config, root, specifier, workspaces }));
2173
2286
  }
@@ -2190,13 +2303,13 @@ var trainVersion = async (root, specifiers) => {
2190
2303
  };
2191
2304
 
2192
2305
  // src/observability.ts
2193
- import { readFileSync as readFileSync12 } from "fs";
2194
- import { join as join12 } from "path";
2306
+ import { readFileSync as readFileSync13 } from "fs";
2307
+ import { join as join13 } from "path";
2195
2308
  var GEONOSIS_FILE2 = "geonosis.json";
2196
2309
  var REACHES_NOTHING = /* @__PURE__ */ new Set(["console", "memory", "noop", "none", "null", "swallowing"]);
2197
2310
  var DEFAULT_MAX_AGE_SECONDS = 3600;
2198
2311
  var HEAD_TIMEOUT_MS = 3e3;
2199
- var finding8 = (verdict, subject, message) => ({
2312
+ var finding9 = (verdict, subject, message) => ({
2200
2313
  check: "observability",
2201
2314
  message,
2202
2315
  subject,
@@ -2205,7 +2318,7 @@ var finding8 = (verdict, subject, message) => ({
2205
2318
  var readGeonosis2 = (root) => {
2206
2319
  let text;
2207
2320
  try {
2208
- text = readFileSync12(join12(root, GEONOSIS_FILE2), "utf8");
2321
+ text = readFileSync13(join13(root, GEONOSIS_FILE2), "utf8");
2209
2322
  } catch {
2210
2323
  return { present: false };
2211
2324
  }
@@ -2223,25 +2336,25 @@ var readGeonosis2 = (root) => {
2223
2336
  var exporterFinding = (config) => {
2224
2337
  const sink = config.sink;
2225
2338
  if (typeof sink !== "string" || sink.trim() === "") {
2226
- return finding8(
2339
+ return finding9(
2227
2340
  "FAIL",
2228
2341
  GEONOSIS_FILE2,
2229
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"
2230
2343
  );
2231
2344
  }
2232
2345
  if (REACHES_NOTHING.has(sink.toLowerCase())) {
2233
- return finding8(
2346
+ return finding9(
2234
2347
  "WARN",
2235
2348
  GEONOSIS_FILE2,
2236
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.`
2237
2350
  );
2238
2351
  }
2239
- return finding8("OK", GEONOSIS_FILE2, `the configured sink is "${sink}"`);
2352
+ return finding9("OK", GEONOSIS_FILE2, `the configured sink is "${sink}"`);
2240
2353
  };
2241
2354
  var reachableFinding = async (config) => {
2242
2355
  const endpoint = config.endpoint;
2243
2356
  if (typeof endpoint !== "string" || endpoint.trim() === "") {
2244
- return finding8(
2357
+ return finding9(
2245
2358
  "SKIP",
2246
2359
  GEONOSIS_FILE2,
2247
2360
  "no observability.endpoint was named, so whether the exporter is reachable was not asked"
@@ -2251,13 +2364,13 @@ var reachableFinding = async (config) => {
2251
2364
  const timer = setTimeout(() => controller.abort(), HEAD_TIMEOUT_MS);
2252
2365
  try {
2253
2366
  const response = await fetch(endpoint, { method: "HEAD", signal: controller.signal });
2254
- return finding8(
2367
+ return finding9(
2255
2368
  "OK",
2256
2369
  GEONOSIS_FILE2,
2257
2370
  `${endpoint} is reachable \u2014 it answered ${response.status} to a HEAD`
2258
2371
  );
2259
2372
  } catch (error) {
2260
- return finding8(
2373
+ return finding9(
2261
2374
  "FAIL",
2262
2375
  GEONOSIS_FILE2,
2263
2376
  `${endpoint} is not reachable from here: ${error.message}. Every report this repo sends is going into that.`
@@ -2269,7 +2382,7 @@ var reachableFinding = async (config) => {
2269
2382
  var ageFinding = (config, root, now) => {
2270
2383
  const file = config.lastEventFile;
2271
2384
  if (typeof file !== "string" || file.trim() === "") {
2272
- return finding8(
2385
+ return finding9(
2273
2386
  "SKIP",
2274
2387
  GEONOSIS_FILE2,
2275
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."
@@ -2278,27 +2391,27 @@ var ageFinding = (config, root, now) => {
2278
2391
  const maxAgeSeconds = typeof config.maxAgeSeconds === "number" && config.maxAgeSeconds > 0 ? config.maxAgeSeconds : DEFAULT_MAX_AGE_SECONDS;
2279
2392
  let record;
2280
2393
  try {
2281
- record = JSON.parse(readFileSync12(join12(root, file), "utf8"));
2394
+ record = JSON.parse(readFileSync13(join13(root, file), "utf8"));
2282
2395
  } catch (error) {
2283
- return finding8(
2396
+ return finding9(
2284
2397
  "FAIL",
2285
2398
  file,
2286
2399
  `the last event file could not be read: ${error.message}. A sink that has never written one has never captured anything.`
2287
2400
  );
2288
2401
  }
2289
2402
  if (typeof record.at !== "number" || !Number.isFinite(record.at)) {
2290
- return finding8(
2403
+ return finding9(
2291
2404
  "FAIL",
2292
2405
  file,
2293
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'
2294
2407
  );
2295
2408
  }
2296
2409
  const ageSeconds = Math.round((now - record.at) / 1e3);
2297
- return ageSeconds > maxAgeSeconds ? finding8(
2410
+ return ageSeconds > maxAgeSeconds ? finding9(
2298
2411
  "FAIL",
2299
2412
  file,
2300
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.`
2301
- ) : finding8(
2414
+ ) : finding9(
2302
2415
  "OK",
2303
2416
  file,
2304
2417
  `the last event arrived ${ageSeconds}s ago, inside the ${maxAgeSeconds}s window`
@@ -2307,16 +2420,16 @@ var ageFinding = (config, root, now) => {
2307
2420
  var probeFinding = (config) => {
2308
2421
  const probe = config.probe;
2309
2422
  if (typeof probe === "string" && probe.trim() !== "") {
2310
- 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}"`);
2311
2424
  }
2312
2425
  if (typeof config.lastEventFile === "string" && config.lastEventFile.trim() !== "") {
2313
- return finding8(
2426
+ return finding9(
2314
2427
  "OK",
2315
2428
  GEONOSIS_FILE2,
2316
2429
  "no probe command, but a last event file is read above, so something does look at this exporter"
2317
2430
  );
2318
2431
  }
2319
- return finding8(
2432
+ return finding9(
2320
2433
  "WARN",
2321
2434
  GEONOSIS_FILE2,
2322
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."
@@ -2329,7 +2442,7 @@ var checkObservability = async ({
2329
2442
  const read = readGeonosis2(root);
2330
2443
  if (read.error !== void 0) {
2331
2444
  return [
2332
- finding8(
2445
+ finding9(
2333
2446
  "FAIL",
2334
2447
  GEONOSIS_FILE2,
2335
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.`
@@ -2338,7 +2451,7 @@ var checkObservability = async ({
2338
2451
  }
2339
2452
  if (!read.present || read.config === void 0) {
2340
2453
  return [
2341
- finding8(
2454
+ finding9(
2342
2455
  "SKIP",
2343
2456
  GEONOSIS_FILE2,
2344
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.`
@@ -2355,13 +2468,28 @@ var checkObservability = async ({
2355
2468
  };
2356
2469
 
2357
2470
  // src/runner.ts
2358
- import { existsSync as existsSync10, readdirSync as readdirSync5, readFileSync as readFileSync13 } from "fs";
2359
- import { join as join13 } 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";
2360
2473
  var TEST_FAILURES = "testFailures";
2361
2474
  var RUNS_A_RUNNER = /(?:^|[\s;&|(])(?:npx\s+|bunx\s+|pnpm\s+(?:exec\s+)?)?(?:vitest|bun\s+test)(?:\s|$)/;
2362
2475
  var RUNS_BUN_TEST = /(?:^|[\s;&|(])(?:bunx\s+)?bun\s+test(?:\s|$)/;
2363
- var WRITES_A_REPORT = /--reporter[= ]\S*json|--outputFile/i;
2364
- 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) => ({
2365
2493
  check: "runner",
2366
2494
  message,
2367
2495
  subject,
@@ -2379,20 +2507,21 @@ var reportingCounters = (ratchet) => (ratchet?.counters ?? []).filter(
2379
2507
  (entry) => entry.counter === TEST_FAILURES && stringOf(entry.report) !== ""
2380
2508
  );
2381
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);
2382
2511
  var PROVES = /--prove\b/;
2383
2512
  var everythingRun2 = (root, workspaces) => {
2384
2513
  const scripts = workspaces.flatMap((one) => Object.values(one.manifest.scripts ?? {}));
2385
- const dir = join13(root, ".github/workflows");
2386
- const workflows = (existsSync10(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) => {
2387
2516
  try {
2388
- return readFileSync13(join13(dir, name), "utf8");
2517
+ return readFileSync14(join14(dir, name), "utf8");
2389
2518
  } catch {
2390
2519
  return "";
2391
2520
  }
2392
2521
  });
2393
2522
  let tiers = {};
2394
2523
  try {
2395
- const config = JSON.parse(readFileSync13(join13(root, "geonosis.json"), "utf8"));
2524
+ const config = JSON.parse(readFileSync14(join14(root, "geonosis.json"), "utf8"));
2396
2525
  tiers = config.verify ?? {};
2397
2526
  } catch {
2398
2527
  }
@@ -2410,7 +2539,7 @@ var proveIsWired = ({
2410
2539
  const { run, tiers } = everythingRun2(root, workspaces);
2411
2540
  if (PROVES.test(run)) {
2412
2541
  return [
2413
- finding9(
2542
+ finding10(
2414
2543
  RATCHET_FILE,
2415
2544
  "OK",
2416
2545
  `${ratchet.counters.length} counter(s), and something here runs geonosis-ratchet --prove`
@@ -2419,7 +2548,7 @@ var proveIsWired = ({
2419
2548
  }
2420
2549
  const where = tiers.at(-1) ?? "full";
2421
2550
  return [
2422
- finding9(
2551
+ finding10(
2423
2552
  RATCHET_FILE,
2424
2553
  "WARN",
2425
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`
@@ -2437,36 +2566,53 @@ var checkRunner = ({
2437
2566
  if (script === "") return [];
2438
2567
  const subject = workspace.relative === "" ? "package.json" : `${workspace.relative}/package.json`;
2439
2568
  const said2 = (verdict, message) => [
2440
- finding9(subject, verdict, message)
2569
+ finding10(subject, verdict, message)
2441
2570
  ];
2442
- 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)) {
2443
2575
  return said2(
2444
2576
  "OK",
2445
- `"${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`
2446
2578
  );
2447
2579
  }
2448
2580
  if (testFilesUnder(workspace.dir).length === 0) {
2449
2581
  return said2(
2450
2582
  "SKIP",
2451
- `"${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`
2452
2584
  );
2453
2585
  }
2454
- if (WRITES_A_REPORT.test(script)) {
2455
- 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
+ );
2456
2591
  }
2592
+ const written = OUTPUT_FILE.exec(judged)?.[1];
2457
2593
  const counter = reading.find((entry) => covers(stringOf(entry.command), workspace, workspaces));
2458
2594
  if (counter !== void 0) {
2459
- 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
+ );
2460
2600
  }
2461
- if (RUNS_BUN_TEST.test(script)) {
2601
+ if (RUNS_BUN_TEST.test(judged)) {
2462
2602
  return said2(
2463
2603
  "OK",
2464
- `"${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}.`
2465
2611
  );
2466
2612
  }
2467
2613
  return said2(
2468
2614
  "WARN",
2469
- `"${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.`
2470
2616
  );
2471
2617
  });
2472
2618
  return [...proveIsWired({ ratchet, root, workspaces }), ...perWorkspace2];
@@ -2474,7 +2620,7 @@ var checkRunner = ({
2474
2620
 
2475
2621
  // src/doctor.ts
2476
2622
  import { homedir as homedir3 } from "os";
2477
- import { join as join15 } from "path";
2623
+ import { join as join17 } from "path";
2478
2624
  import { resolveOxlint } from "@geonosis/lint-parity";
2479
2625
 
2480
2626
  // src/engine.ts
@@ -2538,15 +2684,80 @@ var checkEngines = async ({
2538
2684
  });
2539
2685
  };
2540
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
+
2541
2752
  // src/rails.ts
2542
- import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
2543
- import { join as join14 } from "path";
2753
+ import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
2754
+ import { join as join16 } from "path";
2544
2755
  var GEONOSIS2 = "geonosis.json";
2545
2756
  var PROJECT = ".claude/settings.json";
2546
2757
  var LOCAL = ".claude/settings.local.json";
2547
2758
  var RUN_RECORD = ".geonosis/rails-run.json";
2548
2759
  var managedSettingsPath = (platform = process.platform) => platform === "darwin" ? "/Library/Application Support/ClaudeCode/managed-settings.json" : "/etc/claude-code/managed-settings.json";
2549
- var finding10 = (subject, verdict, message) => ({
2760
+ var finding11 = (subject, verdict, message) => ({
2550
2761
  check: "rails",
2551
2762
  message,
2552
2763
  subject,
@@ -2566,10 +2777,10 @@ var networkIn = (parsed) => {
2566
2777
  };
2567
2778
  };
2568
2779
  var sourceAt = (path, label) => {
2569
- if (!existsSync11(path)) return { network: void 0, path: label, unreadable: false };
2780
+ if (!existsSync12(path)) return { network: void 0, path: label, unreadable: false };
2570
2781
  try {
2571
2782
  return {
2572
- network: networkIn(JSON.parse(readFileSync14(path, "utf8"))),
2783
+ network: networkIn(JSON.parse(readFileSync15(path, "utf8"))),
2573
2784
  path: label,
2574
2785
  unreadable: false
2575
2786
  };
@@ -2578,11 +2789,11 @@ var sourceAt = (path, label) => {
2578
2789
  }
2579
2790
  };
2580
2791
  var declaredEgress = (root) => {
2581
- const at = join14(root, GEONOSIS2);
2582
- if (!existsSync11(at)) return void 0;
2792
+ const at = join16(root, GEONOSIS2);
2793
+ if (!existsSync12(at)) return void 0;
2583
2794
  let parsed;
2584
2795
  try {
2585
- parsed = JSON.parse(readFileSync14(at, "utf8"));
2796
+ parsed = JSON.parse(readFileSync15(at, "utf8"));
2586
2797
  } catch {
2587
2798
  return void 0;
2588
2799
  }
@@ -2594,10 +2805,10 @@ var declaredEgress = (root) => {
2594
2805
  };
2595
2806
  };
2596
2807
  var deniedEgress = (root) => {
2597
- const at = join14(root, RUN_RECORD);
2598
- if (!existsSync11(at)) {
2808
+ const at = join16(root, RUN_RECORD);
2809
+ if (!existsSync12(at)) {
2599
2810
  return [
2600
- finding10(
2811
+ finding11(
2601
2812
  "deniedEgress",
2602
2813
  "SKIP",
2603
2814
  `there is no ${RUN_RECORD} here, so no run has been recorded for this to be a gate over`
@@ -2606,10 +2817,10 @@ var deniedEgress = (root) => {
2606
2817
  }
2607
2818
  let parsed;
2608
2819
  try {
2609
- parsed = JSON.parse(readFileSync14(at, "utf8"));
2820
+ parsed = JSON.parse(readFileSync15(at, "utf8"));
2610
2821
  } catch (error) {
2611
2822
  return [
2612
- finding10(
2823
+ finding11(
2613
2824
  "deniedEgress",
2614
2825
  "FAIL",
2615
2826
  `${RUN_RECORD} is not readable JSON (${error.message}) \u2014 a record nobody can read is not a record of zero denied attempts`
@@ -2618,11 +2829,11 @@ var deniedEgress = (root) => {
2618
2829
  }
2619
2830
  const denied = isRecord3(parsed) && Array.isArray(parsed["deniedEgress"]) ? parsed["deniedEgress"] : [];
2620
2831
  if (denied.length === 0) {
2621
- return [finding10("deniedEgress", "OK", "no run recorded a denied egress attempt")];
2832
+ return [finding11("deniedEgress", "OK", "no run recorded a denied egress attempt")];
2622
2833
  }
2623
2834
  const hosts = denied.map((one) => isRecord3(one) && typeof one["host"] === "string" ? one["host"] : "(unnamed)").join(", ");
2624
2835
  return [
2625
- finding10(
2836
+ finding11(
2626
2837
  "deniedEgress",
2627
2838
  "FAIL",
2628
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`
@@ -2637,7 +2848,7 @@ var checkRails = ({
2637
2848
  const declared = declaredEgress(root);
2638
2849
  if (declared === void 0 || declared.allow.length === 0) {
2639
2850
  return [
2640
- finding10(
2851
+ finding11(
2641
2852
  `${GEONOSIS2} \u2192 rails.egress`,
2642
2853
  "SKIP",
2643
2854
  "this repo declares no egress allowlist, so there is no rendered setting for this to read back"
@@ -2647,11 +2858,11 @@ var checkRails = ({
2647
2858
  const sources = [
2648
2859
  sourceAt(managedSettings, managedSettings),
2649
2860
  sourceAt(userSettings, userSettings),
2650
- sourceAt(join14(root, PROJECT), PROJECT),
2651
- sourceAt(join14(root, LOCAL), LOCAL)
2861
+ sourceAt(join16(root, PROJECT), PROJECT),
2862
+ sourceAt(join16(root, LOCAL), LOCAL)
2652
2863
  ];
2653
2864
  const unreadable = sources.filter((one) => one.unreadable).map(
2654
- (one) => finding10(
2865
+ (one) => finding11(
2655
2866
  one.path,
2656
2867
  "FAIL",
2657
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"
@@ -2666,7 +2877,7 @@ var checkRails = ({
2666
2877
  return [
2667
2878
  ...unreadable,
2668
2879
  ...declared.allow.map(
2669
- (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(
2670
2881
  domain,
2671
2882
  "FAIL",
2672
2883
  `declared in ${GEONOSIS2} \u2192 rails.egress and absent from every settings file that loads, so the run has no allowance for it.${why}`
@@ -2746,7 +2957,7 @@ var exercisedOf = ({
2746
2957
  workspaces
2747
2958
  }) => Promise.all(
2748
2959
  configs.flatMap(
2749
- (config) => config.jsPlugins.filter((name) => name.startsWith(SCOPE)).map(async (specifier) => {
2960
+ (config) => config.jsPlugins.filter((name) => name.startsWith(SCOPE2)).map(async (specifier) => {
2750
2961
  let corpus;
2751
2962
  let entry;
2752
2963
  try {
@@ -2763,6 +2974,7 @@ var exercisedOf = ({
2763
2974
  ];
2764
2975
  }
2765
2976
  const own = repoCorpus !== void 0 && config.relative === CONFIG_FILE ? repoCorpus : void 0;
2977
+ const grants = await grantsOf(entry).catch(() => ({}));
2766
2978
  const [reach, engines] = await Promise.all([
2767
2979
  checkExercised({
2768
2980
  config,
@@ -2776,14 +2988,14 @@ var exercisedOf = ({
2776
2988
  // inside it — one is about the corpus, the other about this repo's own manifests.
2777
2989
  checkEngines({ config, entry, workspaces })
2778
2990
  ]);
2779
- return [reach, ...engines];
2991
+ return [reach, ...engines, ...checkPathGrants({ config, grants, root })];
2780
2992
  })
2781
2993
  )
2782
2994
  );
2783
2995
  var requiredOptionsOf = async (configs) => {
2784
2996
  const found = await Promise.all(
2785
2997
  configs.flatMap(
2786
- (config) => config.jsPlugins.filter((name) => name.startsWith(SCOPE)).map(async (specifier) => {
2998
+ (config) => config.jsPlugins.filter((name) => name.startsWith(SCOPE2)).map(async (specifier) => {
2787
2999
  let entry;
2788
3000
  try {
2789
3001
  entry = resolveFrom(config.dir, specifier);
@@ -2871,7 +3083,8 @@ var runDoctor = async ({
2871
3083
  ["observability", () => checkObservability({ now: Date.now(), root })],
2872
3084
  ["drift", () => checkDrift({ root, workspaces })],
2873
3085
  ["deployed", () => checkDeployed({ root })],
2874
- ["rails", () => checkRails({ root, userSettings: join15(home, USER_SETTINGS) })]
3086
+ ["rails", () => checkRails({ root, userSettings: join17(home, USER_SETTINGS) })],
3087
+ ["exams", () => checkExams({ root, workspaces })]
2875
3088
  ];
2876
3089
  const collected = [];
2877
3090
  const ran = [];
@@ -2897,6 +3110,7 @@ var ABOUT = {
2897
3110
  deployed: "what the pipeline reported deploying is what the tree declares",
2898
3111
  drift: "the gates that were set up and are no longer running",
2899
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",
2900
3114
  exercised: "every enabled rule fires on at least one corpus file",
2901
3115
  group: "the packages published on one version resolve to one version",
2902
3116
  loaded: "the plugin oxlint would load is the one the manifest pins",
@@ -2965,11 +3179,12 @@ export {
2965
3179
  ENVELOPES_DIR,
2966
3180
  NO_ENVELOPES,
2967
3181
  checkEnvelopes,
3182
+ checkExams,
2968
3183
  INSTALLED_PLUGINS,
2969
3184
  PLUGIN_NAME,
2970
3185
  installedPluginVersions,
2971
3186
  checkClaudePlugin,
2972
- SCOPE,
3187
+ SCOPE2 as SCOPE,
2973
3188
  satisfies,
2974
3189
  declaredFor,
2975
3190
  checkLoaded,
@@ -4,7 +4,7 @@ import {
4
4
  formatDoctor,
5
5
  formatJson,
6
6
  runDoctor
7
- } from "./chunk-DR5P4QYC.js";
7
+ } from "./chunk-GWVSC7GZ.js";
8
8
 
9
9
  // src/doctor-cli.ts
10
10
  import { fstatSync, statSync } from "fs";
@@ -129,6 +129,12 @@ var ABOUT = {
129
129
  parity run over a tree the second config ignored. Each published a numerator and no
130
130
  denominator. Reads the files and imports nothing of the tools that wrote them; no
131
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.`,
132
138
  exercised: `Every rule a config enables, against the corpus the loaded plugin ships. A rule at
133
139
  "error" that can never fire is indistinguishable from a clean tree. The line says what
134
140
  its number is evidence ABOUT \u2014 the corpus and the probes, never this tree \u2014 and a rule
@@ -190,7 +196,7 @@ plugin-route-namespaced on its package root, while the shipped corpus says acme
190
196
 
191
197
  --print-config-shape every file this reads, and the keys it reads out of each
192
198
 
193
- 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
194
200
  run read, with the rest excused by name, so a --only run can never read as a verdict about the whole
195
201
  tree. The envelope check SKIPs that one file \u2014 this run is the one writing it.
196
202
 
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
@@ -389,6 +389,23 @@ declare const corpusOfPlugin: (from: string, specifier: string) => string;
389
389
  */
390
390
  declare const relativeToRoot: (root: string, path: string) => string;
391
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
+
392
409
  /**
393
410
  * Whether a test runner's exit code is a verdict — asked of the runner the script actually names,
394
411
  * not of runners in general.
@@ -406,4 +423,4 @@ declare const checkRunner: ({ ratchet, root, workspaces, }: {
406
423
  workspaces: Workspace[];
407
424
  }) => Finding[];
408
425
 
409
- 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, 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 };
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
@@ -22,6 +22,7 @@ import {
22
22
  checkDeployed,
23
23
  checkDrift,
24
24
  checkEnvelopes,
25
+ checkExams,
25
26
  checkExercised,
26
27
  checkGroup,
27
28
  checkLoaded,
@@ -47,7 +48,7 @@ import {
47
48
  resolveFrom,
48
49
  runDoctor,
49
50
  satisfies
50
- } from "./chunk-DR5P4QYC.js";
51
+ } from "./chunk-GWVSC7GZ.js";
51
52
  export {
52
53
  CHECKS,
53
54
  COMPOSITION_ROOT,
@@ -72,6 +73,7 @@ export {
72
73
  checkDeployed,
73
74
  checkDrift,
74
75
  checkEnvelopes,
76
+ checkExams,
75
77
  checkExercised,
76
78
  checkGroup,
77
79
  checkLoaded,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geonosis/doctor",
3
- "version": "2.2.0",
3
+ "version": "2.3.1",
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.2.0"
38
+ "@geonosis/lint-parity": "2.3.1"
39
39
  },
40
40
  "devDependencies": {
41
- "@geonosis/ratchet": "2.2.0"
41
+ "@geonosis/ratchet": "2.3.1"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "oxlint": ">=1.77"