@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 +25 -2
- package/dist/{chunk-R2R2OYH3.js → chunk-GWVSC7GZ.js} +398 -92
- package/dist/doctor-cli.js +21 -6
- package/dist/index.d.ts +57 -4
- package/dist/index.js +11 -1
- package/package.json +3 -3
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
|
|
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
|
|
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
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
|
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
|
|
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/
|
|
1899
|
-
import { readFileSync as readFileSync10 } from "fs";
|
|
1900
|
-
import {
|
|
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
|
|
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) =>
|
|
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
|
|
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 ?
|
|
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
|
-
|
|
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 [
|
|
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 [
|
|
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
|
-
|
|
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 }) =>
|
|
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(
|
|
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(
|
|
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
|
|
2124
|
-
import { join as
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
2394
|
+
record = JSON.parse(readFileSync13(join13(root, file), "utf8"));
|
|
2212
2395
|
} catch (error) {
|
|
2213
|
-
return
|
|
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
|
|
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 ?
|
|
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
|
-
) :
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2289
|
-
import { join as
|
|
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
|
|
2294
|
-
var
|
|
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 =
|
|
2316
|
-
const workflows = (
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2569
|
+
finding10(subject, verdict, message)
|
|
2371
2570
|
];
|
|
2372
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
2385
|
-
return said2(
|
|
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
|
-
|
|
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(
|
|
2601
|
+
if (RUNS_BUN_TEST.test(judged)) {
|
|
2392
2602
|
return said2(
|
|
2393
2603
|
"OK",
|
|
2394
|
-
|
|
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
|
-
|
|
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
|
|
2407
|
-
import { join as
|
|
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)))
|
|
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
|
|
2457
|
-
import { join as
|
|
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
|
|
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 (!
|
|
2780
|
+
if (!existsSync12(path)) return { network: void 0, path: label, unreadable: false };
|
|
2484
2781
|
try {
|
|
2485
2782
|
return {
|
|
2486
|
-
network: networkIn(JSON.parse(
|
|
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 =
|
|
2496
|
-
if (!
|
|
2792
|
+
const at = join16(root, GEONOSIS2);
|
|
2793
|
+
if (!existsSync12(at)) return void 0;
|
|
2497
2794
|
let parsed;
|
|
2498
2795
|
try {
|
|
2499
|
-
parsed = JSON.parse(
|
|
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 =
|
|
2512
|
-
if (!
|
|
2808
|
+
const at = join16(root, RUN_RECORD);
|
|
2809
|
+
if (!existsSync12(at)) {
|
|
2513
2810
|
return [
|
|
2514
|
-
|
|
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(
|
|
2820
|
+
parsed = JSON.parse(readFileSync15(at, "utf8"));
|
|
2524
2821
|
} catch (error) {
|
|
2525
2822
|
return [
|
|
2526
|
-
|
|
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 [
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
2565
|
-
sourceAt(
|
|
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) =>
|
|
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) ?
|
|
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(
|
|
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(
|
|
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:
|
|
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
|
-
|
|
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,
|
package/dist/doctor-cli.js
CHANGED
|
@@ -4,15 +4,15 @@ import {
|
|
|
4
4
|
formatDoctor,
|
|
5
5
|
formatJson,
|
|
6
6
|
runDoctor
|
|
7
|
-
} from "./chunk-
|
|
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
|
|
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:
|
|
229
|
-
root:
|
|
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-
|
|
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.
|
|
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.
|
|
38
|
+
"@geonosis/lint-parity": "2.3.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@geonosis/ratchet": "2.
|
|
41
|
+
"@geonosis/ratchet": "2.3.0"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"oxlint": ">=1.77"
|