@geonosis/doctor 1.0.0 → 1.1.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 +99 -11
- package/bin/geonosis-doctor.mjs +24 -1
- package/dist/{chunk-R4AHDFE3.js → chunk-CG2LN4VW.js} +380 -125
- package/dist/doctor-cli.js +11 -3
- package/dist/index.d.ts +45 -12
- package/dist/index.js +7 -1
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
# @geonosis/doctor
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
20
|
+
## The seven checks
|
|
20
21
|
|
|
21
22
|
### `loaded` — declared ≠ loaded
|
|
22
23
|
|
|
@@ -66,6 +67,51 @@ that ships no corpus is a SKIP: it never claimed to be evidence about anything.
|
|
|
66
67
|
A rule enabled without the options it needs makes oxlint refuse. That refusal is reported as the
|
|
67
68
|
finding — with the sentence naming the rule, which takes a second run to obtain (see below).
|
|
68
69
|
|
|
70
|
+
#### The probe a rule declares for itself
|
|
71
|
+
|
|
72
|
+
A rule whose REACH is an option can never fire on a fixed corpus. The fixtures say `acme` and
|
|
73
|
+
`corpus/workflows/`; a repo says its own brand and `packages/workflows/src/`, and the rule reads as
|
|
74
|
+
firing nowhere in a tree that enforces it on every file. Both source repos reported exactly that
|
|
75
|
+
against themselves on 2026-08-30, and both were right about the fact.
|
|
76
|
+
|
|
77
|
+
So an option-gated rule declares a `probe(options)` beside its `fixShape`, returning the smallest
|
|
78
|
+
source that must fire it under the options it was handed, at a path that lands where it looks. When
|
|
79
|
+
a rule fires nowhere, this writes that probe into a **copy of the corpus** — beside the fixtures,
|
|
80
|
+
because a rule that reads a sibling off disk is answering about a tree — and runs it again:
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
OK .oxlintrc.json: 17 enabled, 17 exercised — 1 through its declared probe under this repo's options
|
|
84
|
+
FAIL .oxlintrc.json: 1 enabled rule fires nowhere in the corpus and nothing through the probe it
|
|
85
|
+
declares either: bio/no-unregistered-plugin-dir under [{"roots":…,"registry":…}]
|
|
86
|
+
UNJUDGED .oxlintrc.json: bio/atom-no-deps declares no probe, so its scope does not reach the corpus
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**It does not soften the check.** A probe that was PLACED under this repo's options and still fired
|
|
90
|
+
nothing is a FAIL naming the option VALUES — the question was asked and the answer was no, and the
|
|
91
|
+
values are what a reader has to go and look at.
|
|
92
|
+
|
|
93
|
+
A rule with no probe, or one whose options admit no constructible path, is **UNJUDGED**: the
|
|
94
|
+
question could not be ASKED. That is not a pass — the run still exits non-zero — and it is not a
|
|
95
|
+
failure either, because a FAIL over a rule a repo is enforcing on every file reads as an invitation
|
|
96
|
+
to delete the rule, and one consumer names exactly such a rule as live enforcement in its own law.
|
|
97
|
+
The sentence says why the corpus cannot answer instead.
|
|
98
|
+
|
|
99
|
+
The probes are read off the plugin the config LOADS, the way `loaded` reads its version — this
|
|
100
|
+
package still depends on no rule plugin. A plugin too old to declare any answers with none, and the
|
|
101
|
+
check behaves exactly as it did before they existed.
|
|
102
|
+
|
|
103
|
+
The probe's PATH is half of it. `step-opens-its-own-cell` records zero findings against the shipped
|
|
104
|
+
corpus not because its source is wrong but because the fixture is at `corpus/workflows/` and the
|
|
105
|
+
consumer's `within` says `packages/workflows/src/` — the same bytes at the second path fire. So a
|
|
106
|
+
probe derives its path from the option, never from a fixed corpus location, and
|
|
107
|
+
`lib/probe.ts`'s `pathMatching` builds one back out of the regex and then tests it against that
|
|
108
|
+
regex, refusing with a sentence rather than placing a file the rule will never look at.
|
|
109
|
+
|
|
110
|
+
One rule cannot be probed by a single file and says so: `no-unregistered-plugin-dir` reports a
|
|
111
|
+
directory its registry does not name and reads that registry off disk, so the probe fires only when
|
|
112
|
+
the configured registry is also in the tree. Widening it until it fired would be the rule claiming
|
|
113
|
+
evidence it does not have, which is the defect the rule itself is about.
|
|
114
|
+
|
|
69
115
|
#### The corpus a shipped one cannot be
|
|
70
116
|
|
|
71
117
|
Three rules cannot be answered by the plugin's corpus at all, because their reach is a **repo's own
|
|
@@ -112,8 +158,20 @@ other people are working in.
|
|
|
112
158
|
|
|
113
159
|
`@cloudflare/vitest-pool-workers` exited 0 over suites it had just reported as failing, for weeks,
|
|
114
160
|
in a consumer, so every gate that trusted the status read green over red. A workspace whose `test`
|
|
115
|
-
script runs vitest
|
|
116
|
-
|
|
161
|
+
script runs **vitest** without asking for a JSON report, and which no `testFailures` counter in
|
|
162
|
+
report mode covers, gets a WARN. `--strict` makes it a FAIL.
|
|
163
|
+
|
|
164
|
+
`bun test` is a different answer, because it is a different runner and it was measured: on bun 1.4.0
|
|
165
|
+
a planted failing assertion exits 1, so does an awaited one, so does a throw in `afterAll`, and an
|
|
166
|
+
all-passing run exits 0. Its exit code IS the verdict, so a bun workspace is OK and the line says so
|
|
167
|
+
naming bun. It used to be warned with vitest's sentence — eight lines of it in one consumer's
|
|
168
|
+
report, each blaming a runner the check had not read, which is how a reader learns to skim the
|
|
169
|
+
section and miss the vitest lines beside them that are true.
|
|
170
|
+
|
|
171
|
+
The message names the runner it read. What it does not do is see through a wrapper: a script that
|
|
172
|
+
runs `bun ./scripts/vitest-strict.ts` reads as neither, and that consumer's wrapper is in fact the
|
|
173
|
+
exemplary fix — it spawns vitest with `--reporter=json`, reads the report, and fails on a missing
|
|
174
|
+
one. The verdict is right and the sentence understates it.
|
|
117
175
|
|
|
118
176
|
### `drift` — the gate that is no longer there
|
|
119
177
|
|
|
@@ -128,6 +186,35 @@ The other four ask whether a gate measures what it names. This one asks whether
|
|
|
128
186
|
| `.claude/settings.json` | WARN | nothing installs the kit's plugin, so the gates run and the method does not (D-041) |
|
|
129
187
|
| `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
188
|
|
|
189
|
+
### `observability` — a sink nobody can reach
|
|
190
|
+
|
|
191
|
+
The exporter named by the `observability` block in `geonosis.json`: is a sink configured, is its
|
|
192
|
+
endpoint reachable, and did an event arrive inside `maxAgeSeconds`. Read from the `lastEventFile`
|
|
193
|
+
the sink writes — never by importing the library being checked, because a check that needs the
|
|
194
|
+
library cannot run in the tree where the library is missing.
|
|
195
|
+
|
|
196
|
+
### `deployed` — declared ≠ deployed
|
|
197
|
+
|
|
198
|
+
`wrangler versions upload` applies no triggers. A cron or a route edited in the config is therefore
|
|
199
|
+
silently ignored in production for ever, with a green pipeline; `wrangler secret put` creates AND
|
|
200
|
+
deploys a version outside the gate entirely. Nothing in either source repo measured any of it.
|
|
201
|
+
|
|
202
|
+
`.geonosis/deployed.json` is what the consumer's pipeline writes **after promote** — the versions,
|
|
203
|
+
the crons and routes it applied, the secret names and the binding names. This line compares it
|
|
204
|
+
against what the tree declares: the wrangler configs `geonosis.json` → `release.wrangler` names
|
|
205
|
+
(JSON or JSONC, with `release.wranglerEnv` selecting an environment block, because named
|
|
206
|
+
environments inherit nothing per binding) and `release.secrets`.
|
|
207
|
+
|
|
208
|
+
| | |
|
|
209
|
+
|---|---|
|
|
210
|
+
| the file is absent | **SKIP**, with the sentence: it is written by the pipeline after promote, and its absence is not a pass |
|
|
211
|
+
| `release.wrangler` and `release.secrets` are both empty | **SKIP** — this repo has declared nothing a deployment is supposed to carry |
|
|
212
|
+
| a config it was told to read cannot be read | **FAIL** — a config nobody can parse is a finding, never a skip |
|
|
213
|
+
| anything declared is not deployed, or anything deployed is not declared | **FAIL**, naming each |
|
|
214
|
+
|
|
215
|
+
It reads the file, and imports nothing of `@geonosis/release` — the same wall `observability` keeps,
|
|
216
|
+
held by the kit's own `layer-walls` config, so a repo can install either package alone.
|
|
217
|
+
|
|
131
218
|
## What `--format=unix` does not tell you
|
|
132
219
|
|
|
133
220
|
Measured on oxlint 1.80: a JS plugin rule that throws from `create()` is reported **once per file**,
|
|
@@ -150,7 +237,7 @@ The whole report, for a CI step to read:
|
|
|
150
237
|
|
|
151
238
|
```json
|
|
152
239
|
{
|
|
153
|
-
"counts": { "FAIL": 1, "OK": 12, "SKIP": 2, "WARN": 0 },
|
|
240
|
+
"counts": { "FAIL": 1, "OK": 12, "SKIP": 2, "UNJUDGED": 0, "WARN": 0 },
|
|
154
241
|
"findings": [{ "check": "loaded", "message": "…", "subject": "apps/web/.oxlintrc.json", "verdict": "FAIL" }],
|
|
155
242
|
"ok": false,
|
|
156
243
|
"root": "/repo"
|
|
@@ -160,8 +247,9 @@ The whole report, for a CI step to read:
|
|
|
160
247
|
## What it does not depend on
|
|
161
248
|
|
|
162
249
|
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.
|
|
164
|
-
`@geonosis/
|
|
165
|
-
|
|
250
|
+
dependency tree would be one more copy for the question to be wrong about. Not
|
|
251
|
+
`@geonosis/observability` and not `@geonosis/release` either: both of those lines read a FILE the
|
|
252
|
+
consumer wrote. It depends on `@geonosis/lint-parity` for the reach engine and on nothing else, and
|
|
253
|
+
the kit's own `layer-walls` config holds every one of those walls.
|
|
166
254
|
|
|
167
255
|
Apache-2.0.
|
package/bin/geonosis-doctor.mjs
CHANGED
|
@@ -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
|
-
|
|
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')
|