@geonosis/doctor 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,22 +1,27 @@
1
1
  # @geonosis/doctor
2
2
 
3
- Five questions a version bump is not finished until something has asked. The first four are ways
3
+ Seven questions a version bump is not finished until something has asked. The first four are ways
4
4
  enforcement has reported **green while measuring nothing** — in real repos, for weeks at a time.
5
- The fifth asks whether the gate is still there at all.
5
+ `drift` asks whether the gate is still there at all. The last two ask the same question one layer
6
+ out: whether what a pipeline reported is what actually happened.
6
7
 
7
8
  ```bash
8
9
  pnpm add -D @geonosis/doctor
9
10
  npx geonosis-doctor
10
11
  ```
11
12
 
12
- Run it **after every bump** and **in CI**. It exits 1 when any line is a FAIL, 2 when the run could
13
- not be made at all.
13
+ Run it **after every bump** and **in CI**. It exits 1 when any line is a FAIL or an UNJUDGED — a
14
+ question it could not ask is not a pass — and 2 when the run could not be made at all.
14
15
 
15
16
  ```
16
17
  geonosis-doctor [--root <dir>] [--json] [--strict] [--baseline-against [<ref>]] [--oxlint <path>]
17
18
  ```
18
19
 
19
- ## The five checks
20
+ ## One check, asked alone
21
+
22
+ `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 (during.day, 2026-08-30: `bun add` across eleven workspaces left `packages/workflows` 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.
23
+
24
+ ## The seven checks
20
25
 
21
26
  ### `loaded` — declared ≠ loaded
22
27
 
@@ -66,6 +71,51 @@ that ships no corpus is a SKIP: it never claimed to be evidence about anything.
66
71
  A rule enabled without the options it needs makes oxlint refuse. That refusal is reported as the
67
72
  finding — with the sentence naming the rule, which takes a second run to obtain (see below).
68
73
 
74
+ #### The probe a rule declares for itself
75
+
76
+ A rule whose REACH is an option can never fire on a fixed corpus. The fixtures say `acme` and
77
+ `corpus/workflows/`; a repo says its own brand and `packages/workflows/src/`, and the rule reads as
78
+ firing nowhere in a tree that enforces it on every file. Both source repos reported exactly that
79
+ against themselves on 2026-08-30, and both were right about the fact.
80
+
81
+ So an option-gated rule declares a `probe(options)` beside its `fixShape`, returning the smallest
82
+ source that must fire it under the options it was handed, at a path that lands where it looks. When
83
+ a rule fires nowhere, this writes that probe into a **copy of the corpus** — beside the fixtures,
84
+ because a rule that reads a sibling off disk is answering about a tree — and runs it again:
85
+
86
+ ```
87
+ OK .oxlintrc.json: 17 enabled, 17 exercised — 1 through its declared probe under this repo's options
88
+ FAIL .oxlintrc.json: 1 enabled rule fires nowhere in the corpus and nothing through the probe it
89
+ declares either: bio/no-unregistered-plugin-dir under [{"roots":…,"registry":…}]
90
+ UNJUDGED .oxlintrc.json: bio/atom-no-deps declares no probe, so its scope does not reach the corpus
91
+ ```
92
+
93
+ **It does not soften the check.** A probe that was PLACED under this repo's options and still fired
94
+ nothing is a FAIL naming the option VALUES — the question was asked and the answer was no, and the
95
+ values are what a reader has to go and look at.
96
+
97
+ A rule with no probe, or one whose options admit no constructible path, is **UNJUDGED**: the
98
+ question could not be ASKED. That is not a pass — the run still exits non-zero — and it is not a
99
+ failure either, because a FAIL over a rule a repo is enforcing on every file reads as an invitation
100
+ to delete the rule, and one consumer names exactly such a rule as live enforcement in its own law.
101
+ The sentence says why the corpus cannot answer instead.
102
+
103
+ The probes are read off the plugin the config LOADS, the way `loaded` reads its version — this
104
+ package still depends on no rule plugin. A plugin too old to declare any answers with none, and the
105
+ check behaves exactly as it did before they existed.
106
+
107
+ The probe's PATH is half of it. `step-opens-its-own-cell` records zero findings against the shipped
108
+ corpus not because its source is wrong but because the fixture is at `corpus/workflows/` and the
109
+ consumer's `within` says `packages/workflows/src/` — the same bytes at the second path fire. So a
110
+ probe derives its path from the option, never from a fixed corpus location, and
111
+ `lib/probe.ts`'s `pathMatching` builds one back out of the regex and then tests it against that
112
+ regex, refusing with a sentence rather than placing a file the rule will never look at.
113
+
114
+ One rule cannot be probed by a single file and says so: `no-unregistered-plugin-dir` reports a
115
+ directory its registry does not name and reads that registry off disk, so the probe fires only when
116
+ the configured registry is also in the tree. Widening it until it fired would be the rule claiming
117
+ evidence it does not have, which is the defect the rule itself is about.
118
+
69
119
  #### The corpus a shipped one cannot be
70
120
 
71
121
  Three rules cannot be answered by the plugin's corpus at all, because their reach is a **repo's own
@@ -112,8 +162,20 @@ other people are working in.
112
162
 
113
163
  `@cloudflare/vitest-pool-workers` exited 0 over suites it had just reported as failing, for weeks,
114
164
  in a consumer, so every gate that trusted the status read green over red. A workspace whose `test`
115
- script runs vitest or `bun test` without asking for a JSON report, and which no `testFailures`
116
- counter in report mode covers, gets a WARN. `--strict` makes it a FAIL.
165
+ script runs **vitest** without asking for a JSON report, and which no `testFailures` counter in
166
+ report mode covers, gets a WARN. `--strict` makes it a FAIL.
167
+
168
+ `bun test` is a different answer, because it is a different runner and it was measured: on bun 1.4.0
169
+ a planted failing assertion exits 1, so does an awaited one, so does a throw in `afterAll`, and an
170
+ all-passing run exits 0. Its exit code IS the verdict, so a bun workspace is OK and the line says so
171
+ naming bun. It used to be warned with vitest's sentence — eight lines of it in one consumer's
172
+ report, each blaming a runner the check had not read, which is how a reader learns to skim the
173
+ section and miss the vitest lines beside them that are true.
174
+
175
+ The message names the runner it read. What it does not do is see through a wrapper: a script that
176
+ runs `bun ./scripts/vitest-strict.ts` reads as neither, and that consumer's wrapper is in fact the
177
+ exemplary fix — it spawns vitest with `--reporter=json`, reads the report, and fails on a missing
178
+ one. The verdict is right and the sentence understates it.
117
179
 
118
180
  ### `drift` — the gate that is no longer there
119
181
 
@@ -128,6 +190,35 @@ The other four ask whether a gate measures what it names. This one asks whether
128
190
  | `.claude/settings.json` | WARN | nothing installs the kit's plugin, so the gates run and the method does not (D-041) |
129
191
  | `geonosis.json` blocks | WARN | a block whose package is not installed — nothing reads it — or an installed package with no block, running on defaults nobody chose |
130
192
 
193
+ ### `observability` — a sink nobody can reach
194
+
195
+ The exporter named by the `observability` block in `geonosis.json`: is a sink configured, is its
196
+ endpoint reachable, and did an event arrive inside `maxAgeSeconds`. Read from the `lastEventFile`
197
+ the sink writes — never by importing the library being checked, because a check that needs the
198
+ library cannot run in the tree where the library is missing.
199
+
200
+ ### `deployed` — declared ≠ deployed
201
+
202
+ `wrangler versions upload` applies no triggers. A cron or a route edited in the config is therefore
203
+ silently ignored in production for ever, with a green pipeline; `wrangler secret put` creates AND
204
+ deploys a version outside the gate entirely. Nothing in either source repo measured any of it.
205
+
206
+ `.geonosis/deployed.json` is what the consumer's pipeline writes **after promote** — the versions,
207
+ the crons and routes it applied, the secret names and the binding names. This line compares it
208
+ against what the tree declares: the wrangler configs `geonosis.json` → `release.wrangler` names
209
+ (JSON or JSONC, with `release.wranglerEnv` selecting an environment block, because named
210
+ environments inherit nothing per binding) and `release.secrets`.
211
+
212
+ | | |
213
+ |---|---|
214
+ | the file is absent | **SKIP**, with the sentence: it is written by the pipeline after promote, and its absence is not a pass |
215
+ | `release.wrangler` and `release.secrets` are both empty | **SKIP** — this repo has declared nothing a deployment is supposed to carry |
216
+ | a config it was told to read cannot be read | **FAIL** — a config nobody can parse is a finding, never a skip |
217
+ | anything declared is not deployed, or anything deployed is not declared | **FAIL**, naming each |
218
+
219
+ It reads the file, and imports nothing of `@geonosis/release` — the same wall `observability` keeps,
220
+ held by the kit's own `layer-walls` config, so a repo can install either package alone.
221
+
131
222
  ## What `--format=unix` does not tell you
132
223
 
133
224
  Measured on oxlint 1.80: a JS plugin rule that throws from `create()` is reported **once per file**,
@@ -150,7 +241,7 @@ The whole report, for a CI step to read:
150
241
 
151
242
  ```json
152
243
  {
153
- "counts": { "FAIL": 1, "OK": 12, "SKIP": 2, "WARN": 0 },
244
+ "counts": { "FAIL": 1, "OK": 12, "SKIP": 2, "UNJUDGED": 0, "WARN": 0 },
154
245
  "findings": [{ "check": "loaded", "message": "…", "subject": "apps/web/.oxlintrc.json", "verdict": "FAIL" }],
155
246
  "ok": false,
156
247
  "root": "/repo"
@@ -160,8 +251,9 @@ The whole report, for a CI step to read:
160
251
  ## What it does not depend on
161
252
 
162
253
  Not the rule plugin. This tool asks which plugin a config loads; a copy of that plugin in its own
163
- dependency tree would be one more copy for the question to be wrong about. It depends on
164
- `@geonosis/lint-parity` for the reach engine and on nothing else, and the kit's own `layer-walls`
165
- config holds that wall.
254
+ dependency tree would be one more copy for the question to be wrong about. Not
255
+ `@geonosis/observability` and not `@geonosis/release` either: both of those lines read a FILE the
256
+ consumer wrote. It depends on `@geonosis/lint-parity` for the reach engine and on nothing else, and
257
+ the kit's own `layer-walls` config holds every one of those walls.
166
258
 
167
259
  Apache-2.0.
@@ -1,4 +1,27 @@
1
1
  #!/usr/bin/env node
2
2
  // Committed, so `pnpm install` can link the bin on a fresh clone — before `pnpm build` has
3
3
  // produced dist/. A bin that only exists after a build is a bin that is missing when you need it.
4
- import '../dist/doctor-cli.js'
4
+ //
5
+ // In the repo, `src/` sits beside `dist/`, and a dist older than src answered for a fix it did not
6
+ // carry once (#62). The published package ships no src, so there the check is skipped.
7
+ import { existsSync, readdirSync, statSync } from 'node:fs'
8
+ import { dirname, join } from 'node:path'
9
+ import { fileURLToPath } from 'node:url'
10
+
11
+ const here = dirname(fileURLToPath(import.meta.url))
12
+ const newest = (dir) =>
13
+ existsSync(dir)
14
+ ? readdirSync(dir, { withFileTypes: true }).reduce((most, entry) => {
15
+ const at = join(dir, entry.name)
16
+ return Math.max(most, entry.isDirectory() ? newest(at) : statSync(at).mtimeMs)
17
+ }, 0)
18
+ : 0
19
+ const src = join(here, '..', 'src')
20
+ if (existsSync(src) && newest(src) > newest(join(here, '..', 'dist'))) {
21
+ process.stderr.write(
22
+ 'geonosis-doctor: dist is older than src — run pnpm build before trusting this bin.\n',
23
+ )
24
+ process.exit(2)
25
+ }
26
+
27
+ await import('../dist/doctor-cli.js')