@geonosis/doctor 1.0.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.
@@ -0,0 +1,109 @@
1
+ import {
2
+ formatDoctor,
3
+ formatJson,
4
+ runDoctor
5
+ } from "./chunk-R4AHDFE3.js";
6
+
7
+ // src/doctor-cli.ts
8
+ import { resolve } from "path";
9
+ var USAGE = `geonosis-doctor [--root <dir>] [--json] [--strict] [--baseline-against [<ref>]] [--oxlint <path>]
10
+
11
+ Five questions a version bump is not finished until something has asked. The first four are ways
12
+ enforcement has reported green while measuring nothing; the fifth asks whether it is still there.
13
+
14
+ loaded The plugin oxlint would LOAD from each config's directory, against the version that
15
+ config's workspace DECLARES. oxlint resolves a jsPlugins specifier from the CONFIG
16
+ FILE's directory, so a nested copy left behind by a per-workspace install runs while
17
+ every manifest and the lockfile say otherwise \u2014 one consumer measured a whole bug
18
+ report against 0.3.0 on a repo pinned to 0.4.0. Every copy is found by RESOLUTION,
19
+ never by find: on pnpm the store keeps every version ever installed.
20
+
21
+ exercised Every rule a config enables, against the corpus the loaded plugin ships. A rule at
22
+ "error" that can never fire is indistinguishable from a clean tree.
23
+
24
+ baseline The ratchet's numbers at HEAD against another ref. A ratchet lowers its own baseline
25
+ when a number shrinks, so nothing inside one checkout can see a branch raise one back
26
+ up. OFF unless asked for: it is the only check that runs git, and this tool is run
27
+ over repos other people are working in.
28
+
29
+ runner Workspaces whose test script's exit code is the only verdict. A pool exited 0 over
30
+ suites it had just reported as failing, for weeks, in a consumer.
31
+
32
+ observability
33
+ The exporter named by the "observability" block in geonosis.json: is a sink
34
+ configured, is its endpoint reachable (a HEAD with a short timeout), and did an event
35
+ arrive inside maxAgeSeconds \u2014 read from the lastEventFile the sink writes, never from
36
+ this tool importing the library it is checking. A deploy job reported success having
37
+ deployed nothing and a /health answered ok over a dead database on the same
38
+ afternoon; both left a green build and a silent project.
39
+ drift The gates that were set up and are no longer running: a CI job switched off by a
40
+ condition that can never be true, a test file under a workspace with no test script,
41
+ an integration directory the registry never names, a law over the ceiling its own
42
+ geonosis.json set, no .claude/settings.json installing the plugin, and a
43
+ geonosis.json block whose package is not installed (or the reverse).
44
+
45
+ exercised reads the corpus the loaded plugin ships, and \u2014 when geonosis.json names one under
46
+ doctor.corpus \u2014 the corpus THIS repo ships for its own options. Three rules cannot be answered any
47
+ other way: layer-walls fires on a repo's own layer names, no-brand-names on its brand, and
48
+ plugin-route-namespaced on its package root, while the shipped corpus says acme and layers/core.
49
+
50
+ Exits 1 when any line is a FAIL, 2 when the run could not be made at all. --strict promotes every
51
+ WARN to a FAIL. --json prints the whole report for a CI step to read.`;
52
+ var VALUED = /* @__PURE__ */ new Set(["--oxlint", "--root"]);
53
+ var parseDoctorArgs = (argv, cwd) => {
54
+ const read = {};
55
+ let baseline;
56
+ let json = false;
57
+ let strict = false;
58
+ for (let index = 0; index < argv.length; index += 1) {
59
+ const flag = argv[index] ?? "";
60
+ const value = argv[index + 1];
61
+ if (flag === "--json") {
62
+ json = true;
63
+ continue;
64
+ }
65
+ if (flag === "--strict") {
66
+ strict = true;
67
+ continue;
68
+ }
69
+ if (flag === "--baseline-against") {
70
+ const named = value === void 0 || value.startsWith("--") ? void 0 : value;
71
+ baseline = { ref: named };
72
+ if (named !== void 0) index += 1;
73
+ continue;
74
+ }
75
+ if (!VALUED.has(flag)) throw new Error(`unknown argument "${flag}"`);
76
+ if (value === void 0 || value.startsWith("--")) throw new Error(`${flag} needs a value`);
77
+ read[flag] = value;
78
+ index += 1;
79
+ }
80
+ return {
81
+ ...baseline === void 0 ? {} : { baseline },
82
+ json,
83
+ ...read["--oxlint"] === void 0 ? {} : { oxlint: resolve(cwd, read["--oxlint"]) },
84
+ root: resolve(cwd, read["--root"] ?? cwd),
85
+ strict
86
+ };
87
+ };
88
+ var main = async () => {
89
+ const argv = process.argv.slice(2);
90
+ if (argv.includes("--help") || argv.includes("-h")) {
91
+ process.stdout.write(`${USAGE}
92
+ `);
93
+ return 0;
94
+ }
95
+ const { json, ...options } = parseDoctorArgs(argv, process.cwd());
96
+ const report = await runDoctor(options);
97
+ process.stdout.write(json ? formatJson(report) : formatDoctor(report));
98
+ return report.ok ? 0 : 1;
99
+ };
100
+ try {
101
+ process.exit(await main());
102
+ } catch (error) {
103
+ process.stderr.write(`geonosis-doctor: ${error.message}
104
+ `);
105
+ process.exit(2);
106
+ }
107
+ export {
108
+ parseDoctorArgs
109
+ };
@@ -0,0 +1,255 @@
1
+ /**
2
+ * The five questions a bump is not finished until something has asked, in the order a run asks
3
+ * them. The first four are ways enforcement has reported green while measuring nothing; the fifth
4
+ * asks whether the gate is still there at all.
5
+ */
6
+ declare const CHECKS: readonly ["loaded", "exercised", "baseline", "runner", "drift", "observability"];
7
+ type CheckName = (typeof CHECKS)[number];
8
+ /**
9
+ * `SKIP` is a first-class answer and is printed like any other: a check whose line is missing reads
10
+ * as a check that passed, and that is the failure this whole package is about.
11
+ */
12
+ type Verdict = 'FAIL' | 'OK' | 'SKIP' | 'WARN';
13
+ type Finding = {
14
+ check: CheckName;
15
+ message: string;
16
+ /** The config, manifest, baseline or plugin this line is about, relative to the root. */
17
+ subject: string;
18
+ verdict: Verdict;
19
+ };
20
+ /** A question that could not be asked at all. Never a verdict — a refusal. */
21
+ declare class DoctorError extends Error {
22
+ constructor(message: string);
23
+ }
24
+ type Manifest = {
25
+ dependencies?: Record<string, string>;
26
+ devDependencies?: Record<string, string>;
27
+ name?: string;
28
+ optionalDependencies?: Record<string, string>;
29
+ peerDependencies?: Record<string, string>;
30
+ scripts?: Record<string, string>;
31
+ };
32
+ type Workspace = {
33
+ dir: string;
34
+ manifest: Manifest;
35
+ /** Relative to the root, `/`-separated; the root workspace is the empty string. */
36
+ relative: string;
37
+ };
38
+ type DiscoveredConfig = {
39
+ dir: string;
40
+ /** Why this config could not be read. A config nobody can parse is a finding, never a skip. */
41
+ error?: string;
42
+ jsPlugins: string[];
43
+ path: string;
44
+ relative: string;
45
+ rules: Record<string, unknown>;
46
+ };
47
+ type RatchetConfig = {
48
+ baseline: string;
49
+ counters: Record<string, unknown>[];
50
+ };
51
+ type DoctorOptions = {
52
+ /**
53
+ * Present only when a baseline comparison was asked for, because making one is the one part of
54
+ * this tool that runs git — and the release check runs the doctor inside repos other sessions
55
+ * are working in. `ref` absent means `origin/main` if the repo has one.
56
+ */
57
+ baseline?: {
58
+ ref?: string;
59
+ };
60
+ oxlint?: string;
61
+ root: string;
62
+ strict?: boolean;
63
+ };
64
+ type DoctorReport = {
65
+ counts: Record<Verdict, number>;
66
+ findings: Finding[];
67
+ /** False as soon as one line is a FAIL — after `--strict` has promoted the warnings. */
68
+ ok: boolean;
69
+ root: string;
70
+ };
71
+
72
+ /**
73
+ * `origin/main`, when the repo has one. Never invented: a comparison against a ref that is not
74
+ * there would either crash or, worse, be reported as a comparison that found nothing.
75
+ */
76
+ declare const defaultRef: (root: string) => string | undefined;
77
+ /**
78
+ * The rail a PreToolUse guard never was.
79
+ *
80
+ * A ratchet lowers its OWN baseline whenever a number shrinks, which is what locks a win in — and
81
+ * it is also what lets a branch write any number it likes and stay green on every gate it runs.
82
+ * Nothing inside one checkout can tell the difference. Only the same file at another ref can.
83
+ */
84
+ declare const checkBaseline: ({ ref, root }: {
85
+ ref: string;
86
+ root: string;
87
+ }) => Finding[];
88
+
89
+ declare const CONFIG_FILE = ".oxlintrc.json";
90
+ declare const MANIFEST_FILE = "package.json";
91
+ declare const RATCHET_FILE = "geonosis.ratchet.json";
92
+ /** `/`-separated, so a subject reads the same on every platform and sorts the way a path should. */
93
+ declare const relativePath: (root: string, path: string) => string;
94
+ declare const readConfig: (path: string, root: string) => DiscoveredConfig;
95
+ declare const discoverConfigs: (root: string) => DiscoveredConfig[];
96
+ declare const discoverWorkspaces: (root: string) => Workspace[];
97
+ declare const readRatchet: (root: string) => RatchetConfig | undefined;
98
+
99
+ declare const runDoctor: ({ baseline, oxlint, root, strict, }: DoctorOptions) => Promise<DoctorReport>;
100
+
101
+ /** Which kit package reads which block of `geonosis.json`. */
102
+ declare const READERS: Record<string, string>;
103
+ /**
104
+ * What has drifted since the gates were set up.
105
+ *
106
+ * Every line here is something that reads as green from the outside: a job that cannot run, a suite
107
+ * nothing runs, a directory nothing reaches, a law nobody finishes, a plugin nobody installed, a
108
+ * block nothing reads. The other four checks ask whether a gate measures what it names; this one
109
+ * asks whether it is still there at all.
110
+ */
111
+ declare const checkDrift: ({ readers, root, workspaces, }: {
112
+ /**
113
+ * Which package reads which block. It is a parameter so a test can name one that can NEVER be
114
+ * installed: a fixture that turns on whether some real package happens to be present on the base
115
+ * it runs against goes green for a reason that is not the code, and red the day that package
116
+ * ships — which is exactly what this one did.
117
+ */
118
+ readers?: Record<string, string>;
119
+ root: string;
120
+ workspaces: Workspace[];
121
+ }) => Finding[];
122
+
123
+ /**
124
+ * The rules of one plugin a config actually turns on. A rule NAMED in a config is not a rule
125
+ * enabled by it — `"off"` is how a config records a rule it decided against, and counting those as
126
+ * enabled would report every one of them as firing nowhere, which is exactly what they do.
127
+ */
128
+ declare const enabledRulesOf: (rules: Record<string, unknown>, plugin: string) => string[];
129
+ /**
130
+ * Enabled is not exercised. A rule at "error" over a tree with no violation of it, and a rule at
131
+ * "error" that can never fire at all, produce the identical green — one consumer ran
132
+ * `no-raw-html-atoms` that way for months with 136 raw elements in the tree. The corpus is the one
133
+ * place where the difference is visible, so the question is asked there, under THIS config's
134
+ * options, because the options are usually where the reach went.
135
+ */
136
+ declare const checkExercised: ({ config, corpus, oxlint, repoCorpus, root, }: {
137
+ config: DiscoveredConfig;
138
+ corpus: string;
139
+ oxlint: string;
140
+ /** The corpus this repo ships for its OWN options, when `geonosis.json` declares one. */
141
+ repoCorpus?: string;
142
+ root: string;
143
+ }) => Finding;
144
+
145
+ /** The scope this tool asks about. A config's other plugins are somebody else's release to check. */
146
+ declare const SCOPE = "@geonosis/";
147
+ /**
148
+ * Whether the version that loaded is the version the manifest asked for — or `undefined` when the
149
+ * range is one this cannot read. A range it cannot judge is a REFUSAL, never a pass: "the check ran
150
+ * and said nothing" is the shape every gate in this kit exists to stop.
151
+ */
152
+ declare const satisfies: (version: string, spec: string) => boolean | undefined;
153
+ /**
154
+ * What the repo SAYS this config's plugin should be: the nearest manifest, walking up from the
155
+ * config, that names the package in any dependency block — ending at the root's. A workspace that
156
+ * declares nothing inherits the root's pin, which is what a hoisted install actually installed.
157
+ */
158
+ declare const declaredFor: ({ dir, specifier, workspaces, }: {
159
+ dir: string;
160
+ specifier: string;
161
+ workspaces: Workspace[];
162
+ }) => {
163
+ at: string;
164
+ spec: string;
165
+ } | undefined;
166
+ declare const checkLoaded: ({ configs, root, workspaces, }: {
167
+ configs: DiscoveredConfig[];
168
+ root: string;
169
+ workspaces: Workspace[];
170
+ }) => Promise<Finding[]>;
171
+
172
+ /** The record a sink writes on every capture. `@geonosis/observability` exports the same shape. */
173
+ type LastEventRecord = {
174
+ at?: unknown;
175
+ id?: unknown;
176
+ release?: unknown;
177
+ sink?: unknown;
178
+ };
179
+ type ObservabilityConfig = {
180
+ /** Where reports go. `https://eu.i.posthog.com`, an OTLP collector, a self-hosted host. */
181
+ endpoint?: string;
182
+ /** Where the sink writes its `LastEventRecord`, relative to the root. */
183
+ lastEventFile?: string;
184
+ /** How old the last event may be before this is a finding. */
185
+ maxAgeSeconds?: number;
186
+ /** The command that plants an error and proves it arrived. Named here, never run from here. */
187
+ probe?: string;
188
+ /** What kind of sink this repo runs: `posthog`, `otlp`, `memory`, … */
189
+ sink?: string;
190
+ };
191
+ declare const checkObservability: ({ now, root, }: {
192
+ now: number;
193
+ root: string;
194
+ }) => Promise<Finding[]>;
195
+
196
+ declare const GEONOSIS_FILE = "geonosis.json";
197
+ /**
198
+ * The reach corpus a repo ships for ITS OWN options, if it declares one.
199
+ *
200
+ * `exercised` asks whether an enabled rule can fire at all, against the corpus the PLUGIN ships.
201
+ * That answers for most rules and cannot answer for the ones whose reach is a repo's vocabulary:
202
+ * `layer-walls` fires on this repo's layer names, `no-brand-names` on its brand,
203
+ * `plugin-route-namespaced` on its package root. The plugin's corpus says `acme` and `layers/core`,
204
+ * so it is no evidence either way — and those rules read as firing nowhere in a tree that enforces
205
+ * them perfectly well.
206
+ *
207
+ * A repo answers for its own options by shipping fixtures in its own vocabulary and naming, in that
208
+ * corpus's own manifest, exactly which rules it is evidence about.
209
+ */
210
+ declare const repoCorpusOf: (root: string) => string | undefined;
211
+
212
+ declare const formatDoctor: ({ counts, findings, ok, root }: DoctorReport) => string;
213
+ declare const formatJson: (report: DoctorReport) => string;
214
+
215
+ /**
216
+ * What oxlint would load, asked the only way that answers on every package manager.
217
+ *
218
+ * oxlint resolves a `jsPlugins` specifier from the CONFIG FILE's directory, not the working
219
+ * directory, so the question is asked from a directory and never from the process's cwd. And it is
220
+ * asked of the RESOLVER: counting copies with `find` is a bun/npm check only — on pnpm the entries
221
+ * under `node_modules/@scope/` are symlinks into `.pnpm/`, which keeps every version ever
222
+ * installed, so the same count answers 7 on a correct tree and 0 with the store excluded.
223
+ */
224
+ declare const resolveFrom: (dir: string, specifier: string) => string;
225
+ /** The package a resolved entry point belongs to: up until a manifest claims the name. */
226
+ declare const packageDirOf: (entry: string, name: string) => string;
227
+ /**
228
+ * The version the plugin itself declares — `meta.version`, the number a diagnostic would carry,
229
+ * read by importing the module exactly as oxlint does. The manifest beside it is a different
230
+ * claim, and when the two disagree the one that ran is the one that matters.
231
+ */
232
+ declare const pluginVersionOf: (entry: string) => Promise<string>;
233
+ /** Where a plugin keeps the corpus it is evidence by, if it ships one at all. */
234
+ declare const corpusOfPlugin: (from: string, specifier: string) => string;
235
+ /**
236
+ * A resolved path, said relative to the root. Both sides are realpath'd first: a resolver returns
237
+ * the real path, and on macOS a temp root under `/var/folders` is a symlink to `/private/var`, so
238
+ * the naive answer is a relative path that climbs out of the repo and back in.
239
+ */
240
+ declare const relativeToRoot: (root: string, path: string) => string;
241
+
242
+ /**
243
+ * A test runner's exit code is not a verdict; its summary is.
244
+ *
245
+ * `@cloudflare/vitest-pool-workers` exited 0 over suites it had just reported as failing, for
246
+ * weeks, in a consumer — so every gate that trusted the status read green over red. The fix is a
247
+ * runner that writes its own report and something that reads it; this says which workspaces have
248
+ * neither.
249
+ */
250
+ declare const checkRunner: ({ ratchet, workspaces, }: {
251
+ ratchet?: RatchetConfig;
252
+ workspaces: Workspace[];
253
+ }) => Finding[];
254
+
255
+ export { CHECKS, CONFIG_FILE, type CheckName, type DiscoveredConfig, DoctorError, type DoctorOptions, type DoctorReport, type Finding, GEONOSIS_FILE, type LastEventRecord, MANIFEST_FILE, type Manifest, type ObservabilityConfig, RATCHET_FILE, READERS, type RatchetConfig, SCOPE, type Verdict, type Workspace, checkBaseline, checkDrift, checkExercised, checkLoaded, checkObservability, checkRunner, corpusOfPlugin, declaredFor, defaultRef, discoverConfigs, discoverWorkspaces, enabledRulesOf, formatDoctor, formatJson, packageDirOf, pluginVersionOf, readConfig, readRatchet, relativePath, relativeToRoot, repoCorpusOf, resolveFrom, runDoctor, satisfies };
package/dist/index.js ADDED
@@ -0,0 +1,68 @@
1
+ import {
2
+ CHECKS,
3
+ CONFIG_FILE,
4
+ DoctorError,
5
+ GEONOSIS_FILE,
6
+ MANIFEST_FILE,
7
+ RATCHET_FILE,
8
+ READERS,
9
+ SCOPE,
10
+ checkBaseline,
11
+ checkDrift,
12
+ checkExercised,
13
+ checkLoaded,
14
+ checkObservability,
15
+ checkRunner,
16
+ corpusOfPlugin,
17
+ declaredFor,
18
+ defaultRef,
19
+ discoverConfigs,
20
+ discoverWorkspaces,
21
+ enabledRulesOf,
22
+ formatDoctor,
23
+ formatJson,
24
+ packageDirOf,
25
+ pluginVersionOf,
26
+ readConfig,
27
+ readRatchet,
28
+ relativePath,
29
+ relativeToRoot,
30
+ repoCorpusOf,
31
+ resolveFrom,
32
+ runDoctor,
33
+ satisfies
34
+ } from "./chunk-R4AHDFE3.js";
35
+ export {
36
+ CHECKS,
37
+ CONFIG_FILE,
38
+ DoctorError,
39
+ GEONOSIS_FILE,
40
+ MANIFEST_FILE,
41
+ RATCHET_FILE,
42
+ READERS,
43
+ SCOPE,
44
+ checkBaseline,
45
+ checkDrift,
46
+ checkExercised,
47
+ checkLoaded,
48
+ checkObservability,
49
+ checkRunner,
50
+ corpusOfPlugin,
51
+ declaredFor,
52
+ defaultRef,
53
+ discoverConfigs,
54
+ discoverWorkspaces,
55
+ enabledRulesOf,
56
+ formatDoctor,
57
+ formatJson,
58
+ packageDirOf,
59
+ pluginVersionOf,
60
+ readConfig,
61
+ readRatchet,
62
+ relativePath,
63
+ relativeToRoot,
64
+ repoCorpusOf,
65
+ resolveFrom,
66
+ runDoctor,
67
+ satisfies
68
+ };
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@geonosis/doctor",
3
+ "version": "1.0.0",
4
+ "description": "The adoption doctor — declared ≠ loaded, enabled ≠ exercised, a baseline that grew, a runner whose exit code is the only verdict.",
5
+ "keywords": [
6
+ "oxlint",
7
+ "lint",
8
+ "doctor",
9
+ "monorepo",
10
+ "adoption",
11
+ "ratchet",
12
+ "ci"
13
+ ],
14
+ "homepage": "https://github.com/microcompanies/geonosis/tree/main/packages/doctor",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/microcompanies/geonosis.git",
18
+ "directory": "packages/doctor"
19
+ },
20
+ "license": "Apache-2.0",
21
+ "type": "module",
22
+ "main": "dist/index.js",
23
+ "bin": {
24
+ "geonosis-doctor": "bin/geonosis-doctor.mjs"
25
+ },
26
+ "exports": {
27
+ ".": "./dist/index.js"
28
+ },
29
+ "files": [
30
+ "bin",
31
+ "dist"
32
+ ],
33
+ "dependencies": {
34
+ "@geonosis/lint-parity": "1.0.0"
35
+ },
36
+ "peerDependencies": {
37
+ "oxlint": ">=1.77"
38
+ },
39
+ "engines": {
40
+ "node": ">=22"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "scripts": {
46
+ "build": "tsup",
47
+ "typecheck": "tsc --noEmit"
48
+ }
49
+ }