@geonosis/doctor 2.3.0 → 2.4.2

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/CHANGELOG.md ADDED
@@ -0,0 +1,676 @@
1
+ # @geonosis/doctor
2
+
3
+ ## 2.4.2
4
+
5
+ ### Patch Changes
6
+
7
+ - f3c3487: The plugin-drift line knows the repo that BUILDS the kit, not only a consumer installing a candidate. Between `changeset version` and `changeset publish` the kit's own packages are at a version npm has never seen, and unlike a consumer there is no `file:` override to recognise it by — so the same unavoidable, unactionable WARN fired on the kit's own tree the first cut after the doctor joined `pnpm verify`. A tree that provides the kit's own packages is in that window by construction.
8
+ - @geonosis/lint-parity@2.4.2
9
+
10
+ ## 2.4.1
11
+
12
+ ### Patch Changes
13
+
14
+ - cb783e8: A `geonosis:gated-by:` marker being QUOTED is a mention, not a use. The kit's own CHANGELOG describes the marker in prose — explaining the very fix that taught this check not to read its own generator as a product — and the check read that sentence as a directive naming a gate half a paragraph long. A directive is followed by a command; a literal in markdown or in code is followed by the quote that closes it.
15
+
16
+ Found by `pnpm verify` the first time the doctor ran inside the kit's own gate, which is the reason that step was added one release earlier.
17
+ - @geonosis/lint-parity@2.4.1
18
+
19
+ ## 2.4.0
20
+
21
+ ### Minor Changes
22
+
23
+ - 4962ed6: Every publishable package names `CHANGELOG.md` in `files`, not only the five floors. Measured on the 2.4.0 tarballs before they shipped: **0 of the 22 group packages carried one**, so `geonosis update`'s `BY HAND` block printed nothing for a group bump — the third consecutive release, in the cut where a consumer would have read the sweep, the holder and the Stop-hook fixes. `tooling/floor-changelog-shipped.test.ts` now holds the whole workspace rather than the floors, so a package cannot fall out of the convention silently.
24
+
25
+ The doctor's `exams` check asks one question per EXAM instead of one per package. `@geonosis/db` ships three (`sessionConformance`, `entityConformance`, `singletonConformance`), and importing any one of them anywhere used to answer OK for the whole package — so a consumer running the session exam alone read green while two floors' behaviour was never exercised. That is this check's own docstring failing inside the check written to state it.
26
+
27
+ The plugin-drift line knows a release candidate. A tree whose `overrides`/`resolutions` point a kit package at a `file:`, `link:`, `portal:` or `.tgz` spec is installed from a candidate the registry has never seen, so `claude plugin update` cannot reach that version at all — the WARN was unavoidable and unactionable for the whole life of every RC, in the window a consumer reads the doctor hardest. It now says the packages are a candidate and that the question becomes real when that version publishes.
28
+
29
+ `smoke run` refuses a STALE recording instead of rendering a verdict over it. The 2.4.0 sweep headlined `FAIL … the release broke 1 of their gates` and printed `STALE` as its last line, fifteen lines below: both true, and the FAIL was a consumer frozen two releases back crossing a break declared in the one before, while the tree they actually have had migrated past it and was green. A stale recording cannot answer whether THIS release breaks them — only what it does to a tree nobody has — so it leads the block, names the command that re-takes it, and exits 2, the code for a run that could not be made.
30
+
31
+ ### Patch Changes
32
+
33
+ - bef73fb: Every shim tells a tree that was never built from a build that fell behind. `newest(dist)` answers 0 for a directory that is not there and for one that is merely old, so both conditions were one comparison and one sentence — and the staleness sentence was printed over trees that had never been built at all. It blocked a session's turn in a fresh git worktree, which has no `dist` because `dist` is not committed, with a message about a build going stale. Only one of the two is evidence of a mistake, and the two send a reader to different places.
34
+
35
+ The test that holds it plants BOTH cases and requires different sentences, which the previous one never did: it planted only a stale `dist`, so the check had never been shown a case whose answer should differ. Named for what it is, since the same gap produced two wrong published numbers the same day — a probe never shown a differing case has not been shown to discriminate, which is this repo's own "a gate never seen red has not been shown to measure", one layer down, applied to the thing doing the observing.
36
+
37
+ (This changeset names four packages of the fixed group deliberately: the group versions as one number and a changeset's description lands only in the packages it names, so naming several puts it in more than one place a reader might look. See #356.)
38
+ - a5b2319: The drift check no longer accuses the source that WRITES its marker of carrying one. A file that writes `geonosis:gated-by:` necessarily contains it, so a check that greps a tree for the marker reads its own generator as one of its own products — the kit's own `packages/cli/src/sync.ts` was accused for as long as the marker has existed, and the gate the accusation named was `${SYNC_GATE}`, a command no script anywhere can run. An unexpanded template is now the tell, and such a file gets a SKIP that says it writes the marker rather than a FAIL nobody can act on.
39
+
40
+ The marker also ends at a backtick now, not only at `-->`, `*/` or end of line. Read to end of line, a marker embedded in a template literal swallowed the closing backtick into the gate's NAME, so a repo that genuinely runs its gate matched nothing and was told it does not — a false FAIL, which is the verdict that gets a check switched off.
41
+ - @geonosis/lint-parity@2.4.0
42
+
43
+ ## 2.3.1
44
+
45
+ ### Patch Changes
46
+
47
+ - @geonosis/lint-parity@2.3.1
48
+
49
+ ## 2.3.0
50
+
51
+ ### Minor Changes
52
+
53
+ - 3dac9a7: **A path grant is enumerated, and what it exempts is counted (#139).** Measured from the option
54
+ table, the plugin has exactly three path GRANTS — `time-through-the-door`'s `exempt` and the
55
+ `allow` of `font-roles-only` and `tokens-only-colors`; `within`, `paths`, `roots` and `routers` are
56
+ scopes, not exemptions. Each now takes `exemptModules` beside its pattern: modules named in full,
57
+ matched by path or by the tail a repo writes them as, never as a pattern — the `boundary-time-is-primitive`
58
+ model. The patterns still work; what changed is that they are counted. Each such rule declares
59
+ `meta.grants` (`{ paths, modules }`), so the doctor keeps no list of another package's options, and
60
+ `geonosis-doctor`'s `exercised` line WARNs with the number of files the pattern exempts in THIS tree
61
+ beside the number the enumeration names.
62
+ - 3dac9a7: **`exams`, the doctor's eleventh check (#293).** A floor bump that breaks search is invisible to a
63
+ tier that never runs the search exam. A verify tier declares COMMANDS, not globs, so "the exam runs
64
+ in the fast tier" is not checkable — but whether a test file importing the exam exists at all is.
65
+ For every `@geonosis/*` package a manifest declares, the check reads the exams that package ships
66
+ (every export ending in `Conformance`, off the installed floor rather than a list held here) and
67
+ WARNs, naming them, when no test file imports one. The adoption sweep's consumer row now carries an
68
+ `EXAMS` line listing the exams the recording's own test files run.
69
+ - 3dac9a7: **The `runner` check opens the wrapper a test script names (#67).** A workspace whose `test` script
70
+ is `bun ../../scripts/strict.ts` names no runner in its manifest, and the check called that "runs
71
+ neither vitest nor bun test" — the right verdict for the wrong reason, over the very fix it exists
72
+ to ask for. It now reads ONE level into the local file the command names, judges the runner it finds
73
+ there, and says which file it read. A wrapper that asks the runner for a JSON report and parses that
74
+ report is OK: the exit code is not the verdict there.
75
+
76
+ ### Patch Changes
77
+
78
+ - 3dac9a7: **A counter removed from the baseline is a rule downgrade, not a win (#14).** `checkBaseline`
79
+ compared `here[key] ?? was` against `was`, so a key present at the ref and deleted at HEAD read as
80
+ "no counter grew" — the one shape no gate inside the checkout can see. A key the reference has and
81
+ HEAD does not now FAILs naming the counter and its last number.
82
+ - 3dac9a7: **A JSON report nobody reads is not evidence (#15).** The `runner` check OK'd any test script
83
+ carrying `--outputFile`, so `vitest run --reporter=json --outputFile=/dev/null` passed it. The
84
+ report is now OK only where a `testFailures` counter in report mode covers the workspace — and the
85
+ line says so by path when the counter's `reportPath` or command names the file the script writes.
86
+ Otherwise it is a WARN naming the path: written, read by nothing.
87
+ - 3dac9a7: **One documented spelling (#90).** Every README of a package the metapackage fronts now names
88
+ `geonosis <tool>` — the door that pins the whole toolchain at one version — beside the bare bin, and
89
+ a `tooling/` test holds it. `#230`'s stamp test lands with it: every flag `docs/orchestration.md`'s
90
+ adoption loop names has to carry the version it first ships in, because the loop once told a
91
+ consumer to run a flag the published version did not have.
92
+ - 3dac9a7: **`geonosis explain <rule|counter|tier|package>` (#91).** The effective value of everything a name
93
+ decides, beside the file that decided it: a rule's severity and options out of `.oxlintrc.json` with
94
+ the presets that carry it, a counter's entry and its baseline, a tier's steps, and a package's own
95
+ `--print-config-shape` beside the config files this repo actually has. The doctor's "it runs on its
96
+ defaults, whatever they are" now names that command instead.
97
+ - Updated dependencies [3dac9a7]
98
+ - Updated dependencies [3dac9a7]
99
+ - Updated dependencies [3dac9a7]
100
+ - Updated dependencies [3dac9a7]
101
+ - @geonosis/lint-parity@2.3.0
102
+
103
+ ## 2.2.0
104
+
105
+ ### Minor Changes
106
+
107
+ - d15a654: **A rule names the floor its pattern belongs to, and the doctor says when it is not shipped (#290).**
108
+ `document-sagas-are-generic` and `documents-share-one-table` lint a pattern this kit ships no floor
109
+ for: the rule is the requirement and there is nothing to install behind it, so a second consumer
110
+ hand-writes the floor to satisfy the lint. A rule's `meta` carries `floor` — `{ name, pending }` —
111
+ and the doctor's `exercised` line says "names a pattern the kit does not ship yet (<floor>)" for a
112
+ pending one a config enables. RULES.md carries the generated table, so the inventory says so where
113
+ somebody reads a rule before enabling it, and the entry clears when the floor ships.
114
+ - d15a654: **The doctor asks whether the installed Claude plugin is this release's (#133).** A consumer's npm
115
+ bump moves the packages and never the plugin — the hooks, the agents and the skills live there and
116
+ move on `claude plugin update` — so a repo can be on this release while every session in it runs the
117
+ previous release's hooks, and a refusal added this release cannot fire. The `loaded` check now reads
118
+ `~/.claude/plugins/installed_plugins.json` (measured 2026-09-02: `version: 2`, keyed
119
+ `<plugin>@<marketplace>`, each installation carrying its `version` — the record `claude plugin list`
120
+ answers from) and WARNs when the installed version differs from the train this tree resolves, or
121
+ when the plugin is not installed here at all.
122
+
123
+ ### Patch Changes
124
+
125
+ - d15a654: **An envelope writer refuses a root it has been told is not its (#241).** A suite ran one over the
126
+ SHARED repo root mid-run: parallel suites made the comparison racy, and the file it left behind is
127
+ one the next run reads as real. Every one of the seven writer copies now refuses to write when
128
+ `GEONOSIS_ENVELOPES_FORBIDDEN_ROOT` names the root it was pointed at, saying so and pointing at a
129
+ scratch root instead. The kit's own vitest config sets it to the repo root, so a test that writes an
130
+ envelope there names itself — which is how the offender that filed this row was found again.
131
+ - c4c9330: `runDoctor` takes `home` — where `~/.claude` lives for the plugin registry and the user settings — so a doctor run over a scratch tree is judged against the registry a caller names, never against the machine's own plugin. The kit's scaffolded-tree exam read this machine's plugin (2.1.0) against a tree at 2.2.0 and went red for a drift that was the exam's, not the scaffold's.
132
+ - Updated dependencies [d15a654]
133
+ - @geonosis/lint-parity@2.2.0
134
+
135
+ ## 2.1.0
136
+
137
+ ### Minor Changes
138
+
139
+ - c54d6a9: **The bump moves the floors, and names what no command can do (#221, #225).** `geonosis update --to` repinned the fixed group and left every floor package where it was, with one line saying so. It now takes `--floors latest` and `--floor <package>@<version>` (repeatable): each floor is resolved against the registry with everything else BEFORE a byte is written (#148), a `--floor` naming a package no manifest here declares is refused rather than skipped, and the ones that move become `moved <at> <name> <from> → <to>` in the report and in the printed commit body.
140
+
141
+ **And the doctor answers to the name it carries.** `required-options.ts` emits `check: 'loaded'` and was run by the `exercised` pass, so `geonosis-doctor --only loaded` — what the bump runs — could never print it: measured 2026-09-02 on a planted tree, `--only loaded` said nothing about a rule enabled without its option and `--only exercised` printed it under a heading called "loaded". That is how 2.0.0's new refusals reached a consumer's config with nothing between them and it. The check now runs under `loaded`, and the bump report carries a `BY HAND` block naming every rule and the option it needs. `update --prove` grows both plants and passes on bun and pnpm alike.
142
+ - a016fa4: **The doctor writes its own envelope (#234).** It was the one tool of the six that wrote none — `--json` went to stdout — so a consumer who redirected that stdout into `.geonosis/envelopes/doctor.json` had the same run's `envelope` check read the file the shell had just truncated and FAIL "is not readable JSON: Unexpected end of JSON input", exit 1 over a tree that had passed. Every run now writes the envelope after the checks, in the shape the W32 tools use: the ten checks CONSIDERED against the ones this run read, the rest excused by name, so a `--only` run can never read as a verdict about the whole tree. The envelope check SKIPs that one file — this run is the one writing it — and a run whose stdout IS that file writes no envelope and says so, rather than overwriting the report someone asked for.
143
+
144
+ ### Patch Changes
145
+
146
+ - e0da9a3: **D-060 made mechanical, and the sweep it found (#220).** A published README or dist comment may name a consumer as the SOURCE of a pattern — that is attribution of the user's own products — and may not describe what is inside one. `tooling/provenance.ts` reads every publishable package's README and `dist/`, and fails on a consumer's name followed within the sentence by an internals noun (table, plane, column, schema, migration, incident, outage, postmortem, `apps/`, `packages/`) with no provenance phrase between them; the consumer names come from `geonosis.json`'s declared smoke snapshots, never from a list of its own — the kit's `no-brand-names` rule refuses a brand written into this repo, so a name enters that vocabulary only by being declared a consumer. It found nineteen sentences across nine packages — their file paths, their table and selector names, their line counts, their incident — and every one is reworded rather than stripped of its attribution.
147
+ - ef19211: **A kit omission is not a warning against the consumer (#237).** `exercised` WARNed "the loaded plugin ships no probe … the kit's omission, not this repo's (geonosis #131)" — 18 rules across three workspaces, on every run, in a tree where nothing was the consumer's to fix, and `--strict` promoted each one to a FAIL. A rule the loaded plugin ships no probe for is now a SKIP naming the kit's row: the question could not be asked here at all. A probe the CONFIG hides is unchanged — still UNJUDGED, because there the consumer can widen or confirm.
148
+ - e04c395: **A root declaration a workspace script calls is not unused (#236).** The `group` check read the ROOT's scripts and the committed hooks, and WARNed "the root declares it and no root script calls its bin" over declarations three workspace scripts depend on — `apps/api`'s `test: geonosis-testbed strict`, `apps/web`'s `verify:arch`. Under a hoisted linker the root declaration is exactly what makes those bins resolvable, and "declare it in the workspace whose code imports it" is advice about imports, not bins. The check now reads every workspace's `scripts` before calling a root declaration unused, and a bin a workspace calls is an OK line naming the workspace and the script.
149
+ - 849f72e: **A scaffolded tree earns no WARN of its own (#215).** `create-geonosis` and `geonosis init` left six out of the box, and a scaffold whose own doctor is dirty teaches every repo it makes that a WARN is scenery. The root now declares a `prove` script and the `full` tier names it when the root has one, so the counters are proved rather than only compared; `sync:graph` names the ledger bin the scaffold already runs, from the same one list `finish` runs it from; and `--kit` declares at the root exactly what a registry install would, instead of every package it linked so the bins resolve. Two of the six were the doctor contradicting itself: a `jsPlugins` entry is a caller with no bin to call — pnpm 11.8.0 resolves only DIRECT dependencies from a directory, so dropping that declaration is how a root config stops linting — and a `geonosis.json` block whose reader the declared door brings is read by something, which is the shape #177 asks a root to have.
150
+ - 4c91653: **The engine-presumption walk reads downwards too, and names what it read (#227).** The check asked the manifests from the config's directory UP to the root, so a Medusa consumer whose backend is `apps/backend/` below a root config was told two of the rules that backend exercises presume an engine "no manifest declares" — measured on dielime under 2.0.0. It now asks every manifest the config governs, the workspaces underneath included, and the WARN lists the ones it read: `none of the manifests this config governs (root, apps/backend) declares @medusajs/framework`.
151
+ - c3223cc: **The required-option check says so when everything is in order (#239).** On a tree where every gated rule already carried its option the check printed nothing, so a consumer could not tell "all options present" from "this check does not exist" — and the check is the one thing between a config and a run that lints nothing. Each config that enables a rule of the plugin now gets one OK line, `N rules require options, N carry them`, asked of the rules themselves; the FAIL lines are still the whole answer when one is missing.
152
+ - b7dab40: **The nested-copy FAIL stops prescribing the repair that bricks a pnpm tree (#156).** It said `rm -rf <workspace>/node_modules/@geonosis`, and on pnpm 11.8.0 no install puts the package back after that — plain, `--force` and state-deleted all print "Already up to date" over a tree the package is now missing from. The line now names `geonosis update --to <the version declared>`, says it takes the WHOLE node_modules of every non-root workspace carrying a copy and never the root's, and says out loud what deleting the scope directory alone costs.
153
+ - @geonosis/lint-parity@2.1.0
154
+
155
+ ## 2.0.0
156
+
157
+ ### Minor Changes
158
+
159
+ - 07cc9f2: **Egress: rendered by the rails, read back by the doctor (W24a R3).** `geonosis.json → rails.egress` renders through `geonosis-rails egress --write` into `.claude/settings.json` as `sandbox.network.allowedDomains` / `deniedDomains` — the shape Claude Code 2.1.258 reads, measured off the binary. The doctor gains a tenth check, `rails`, which reads each declared host back out of the settings that ACTUALLY load (managed, user, project, local — Claude Code merges the allow lists) and refuses when it is absent, naming the two measured causes: a settings file that fails validation is silently ignored under `-p`, and a managed `allowManagedDomainsOnly` makes every other scope's allow list ignored. `deniedEgress` in `.geonosis/rails-run.json` is a GATE AT ZERO (D-025) — one entry fails the run, and `tooling/gates-at-zero.test.ts` holds the name out of the ratchet config.
160
+ - 2ed0d01: **The drift line "installed but never composed" now asks it of the floors.** #165's observability
161
+ sentence becomes one case of a general question: a package a repo declares in order to COMPOSE, and
162
+ no source file naming it, is a WARN — the floor packages (`FLOOR_PACKAGES`, held against the
163
+ workspace by `tooling/group-membership.test.ts` exactly as `FIXED_GROUP` is) name the app's
164
+ `src/platform.ts` in the fix, which is the file `create-geonosis --domains` writes. The tree is
165
+ walked once for every package asked about rather than once per package.
166
+ - c43551a: **A missing-option refusal is raised once per run, and the doctor names it before lint runs (#202).** oxlint constructs a rule per FILE, so a bare option-gated rule printed "Error running JS plugin" once per file — during.day's first sight of a version bump was hundreds of unparseable problems and a counter answering CANNOT MEASURE. The refusal now speaks for the first file and the rule is inert for the rest; a tool that asks a rule what it needs (no filename, or the same file twice) still hears it every time. Beside it, `geonosis-doctor` constructs every rule a config enables with the options that config gives it and FAILs by name — `<config> → <rule>`, the option missing — which is the same defect said before any lint starts.
167
+
168
+ ### Patch Changes
169
+
170
+ - c3c40e1: **The root-declares line counts a committed hook as a caller (#209).** during.day's `lefthook.yml` runs `node_modules/.bin/geonosis-ledger` — the exact path the hook-path check REQUIRES, since a bin reached through a version manager has to be found first — and this line called the same declaration unused, so two checks of one tool disagreed about "used". It now reads the hook files the hook-path check already reads.
171
+ - a5e2341: **A bare-run refusal names the rule and its REQUIRED option in a fixed first position (#199).** `biological-architecture/<rule> requires option \`<option>\`.` opens the message, and the thrown error is an `OptionMissing` carrying `rule` and `option` as fields — the sentence after it still quotes every option the rule accepts, which is what made a substring assertion unable to tell a required option from a mentioned one. Two wrong entries fell out of the stronger test the same hour: `worker-handles-are-scoped` refuses on `within` first, not `wrapper`, and `no-raw-html-atoms` names its positional `element→atom map`. The doctor reads the reason by any thrown class rather than by `Error:`.
172
+ - @geonosis/lint-parity@2.0.0
173
+
174
+ ## 1.4.0
175
+
176
+ ### Minor Changes
177
+
178
+ - 7c10970: **`drift` checks every `publicHoistPattern` has its root link (#126).** pnpm dropped the root `node_modules` link for a publicly hoisted workspace package during an unrelated `add`; every install afterwards answered "Already up to date" with a clean `git status`, and the package the pattern exists to make resolvable was not resolvable. The line names the pattern, the missing link, and the repair verbatim — `rm -rf node_modules/.modules.yaml node_modules/.pnpm-workspace-state-v1.json && pnpm install`. Only workspace packages are judged: one exists on disk by construction, so a missing root link for it cannot be "the install never ran". The root package is never asked about. A pattern matching no workspace package, a workspace file without the key, and no workspace file at all are each a SKIP with a sentence. `--print-config-shape` now declares `pnpm-workspace.yaml`, because a file the doctor reads and does not declare is the drift it exists to catch.
179
+ - 0b55a82: **A new `envelope` check: every gate read as many things as it was handed.** For each `.geonosis/envelopes/<tool>.json`, `considered === read + refused + excused`; a mismatch is a FAIL naming the tool and the three numbers, an unreadable or shapeless file is a FAIL rather than a skip, and no envelopes at all is a SKIP that says so out loud. It reads the files and imports nothing of the tools that wrote them — the discipline `deployed` and `observability` already follow — so it runs over a consumer's `.geonosis/` with none of the kit installed. Four denominator bugs landed in one day and every one of them was green; this is the one line that would have caught all four.
180
+ - 143281d: **Exercised answers with the right owner, and asks where the config looks (#137, #131's placement half).** An UNJUDGED whose cause is "the loaded plugin ships no probe" is now a WARN naming the KIT as the owner — a consumer's build no longer goes red for the kit's omission (during.day, 1.3.0: three rules that demonstrably bite). Probes whose declared path no enabling entry claims are REHOMED under a path concretized from the entry's own glob (extension included) and exercised there. And the probe run's config is written INTO the corpus copy — measured on oxlint 1.80: overrides globs anchor at the config file's directory, so a config left at its original path silently deconfigured every scoped override during the probe pass.
181
+
182
+ ### Patch Changes
183
+
184
+ - 38524fb: **`drift` reads `no-unregistered-plugin-dir`'s `registry` in both shapes.** The rule now takes `string | string[]` so it can read a registry split across files (#123). The doctor demanded a string, so a layer configured with the list would have answered `undefined` and printed "the rule is not configured here" over a repo enforcing it — a silent zero, in the tool that exists to catch silent zeros, about the shape the kit itself recommends. The halves are read as one, a half that is not on disk is named, and a directory neither half reaches is still a FAIL.
185
+ - Updated dependencies [c712f3d]
186
+ - @geonosis/lint-parity@1.4.0
187
+
188
+ ## 1.3.0
189
+
190
+ ### Minor Changes
191
+
192
+ - c340de0: **`drift`: a geonosis bin a git hook cannot start (#106).** dielime's first hooked commit was
193
+ "refused" on 2026-08-30 and the ledger never ran: a git hook's PATH is not the shell's — no nvm
194
+ shim, no corepack shim — so `pnpm` was not FOUND, and the sentence its author read was a version
195
+ manager's failure wearing a gate's refusal costume. That is the worst shape a gate has, because it
196
+ is indistinguishable from the gate working. The fix was one word: call
197
+ `./node_modules/.bin/geonosis-ledger` directly.
198
+
199
+ Every committed hook — `lefthook.yml`, `.husky/*`, `.githooks/*` — is now read, and a line running a
200
+ geonosis bin through `pnpm`, `npx`, `bunx` or `yarn` is a FAIL naming the direct call that replaces
201
+ it. A hook file naming no geonosis bin is a SKIP with the sentence, never a vacuous OK.
202
+
203
+ **And the hook `create-geonosis` writes follows its own rule.** It wrote `npx oxfmt` / `bunx oxlint`
204
+ into the `lefthook.yml` it scaffolds — the same shape, printed by the kit, into the file where it
205
+ breaks. Both lines are now `node_modules/.bin/…`, with the reason in a comment above them.
206
+ - c9b63c0: **A probe declares FILES, not a file (#101).** A rule whose reach is an option declares
207
+ `probe(options)` so the doctor can exercise it under the repo's OWN values. One file was not enough
208
+ for a rule that reads a SIBLING off disk: `no-unregistered-plugin-dir` returns silently when the
209
+ registry its options name is not there, so its probe fired nothing — and dielime, enforcing that
210
+ rule at error over four registries, read as a permanent `exercised` FAIL for a gate that was
211
+ working. Their call, that the rule stays and the doctor is expected-red, was right, and it was the
212
+ indictment.
213
+
214
+ `probe` may now answer `{ files: [{ path, source }, …] }`, and `exercised` mounts every file it
215
+ declares into the copy of the corpus. `no-unregistered-plugin-dir` declares the directory under
216
+ `roots` AND the registry at the configured path — a registry carrying no string literal at all, so
217
+ nothing in it can name the directory beside it. Its own test moved from "fires nothing, and says so"
218
+ to the one finding every other probe has to produce, through the real binary.
219
+
220
+ The bare `{ path, source }` is the one-file case of the same contract and is read unchanged. That is
221
+ not tidiness: the doctor reads probes off the plugin build a consumer has INSTALLED, and every build
222
+ published before this shape existed answers in the bare form — refusing it would turn every probe in
223
+ an older install into no probe at all, which reads as an accusation against the repo.
224
+
225
+ The FAIL branch — a probe PLACED under the repo's options that still fires nothing — is now held by
226
+ a planted plugin whose one rule declares a probe and reports nothing, ever. It was held by
227
+ `no-unregistered-plugin-dir`, so fixing that rule would have quietly deleted the coverage.
228
+ - 228566f: **The doctor reads the options oxlint RESOLVES, not the base object beside them (#102).** A
229
+ config's `rules` block is only its first layer. An `overrides[]` entry naming a rule again replaces
230
+ its options for the files its globs claim — and dielime declares two of its three Medusa provider
231
+ registries exactly there, because that is where oxlint reads them. The rule resolved it correctly.
232
+ `drift` read `rules` alone and reported two working fulfillment carriers as directories the base
233
+ registry "never names": one config, two answers, from the tool whose whole subject is gates that
234
+ disagree with what they claim to measure.
235
+
236
+ Every reader that consumes a rule's configuration now takes base + matching overrides, resolved the
237
+ way oxlint resolves them — **measured** against oxlint 1.80, not assumed: an override REPLACES the
238
+ base's options for its files rather than merging into them, and the last of several matching
239
+ overrides wins outright.
240
+
241
+ - `drift`'s plugin-directory check asks each directory which layer governs it and checks it against
242
+ THAT layer's registry and manifests, so a repo with several registries gets a line per registry.
243
+ - `exercised` counts a rule an overrides entry enables — it read "no rule of this plugin is enabled
244
+ here" before, a SKIP from the check whose thesis is that an invisible check reads as a passing one
245
+ — and builds a probe from every configuration the repo gives a rule, not only the first.
246
+ - `@geonosis/lint-parity`'s `rulesNamedBy` reads overrides too: a rule configured only there fired
247
+ in the reach run and had no row in the report about which rules reach anything.
248
+
249
+ A layer's options are only half of what it resolves; the other half is which FILES it governs. A
250
+ probe built from an override's options and written where the RULE looks is a probe for a
251
+ configuration that will not run on it — during.day scopes `no-raw-html-atoms` to its compound and
252
+ cell folders, and the probe lands nowhere near them. When no layer enabling a rule claims where its
253
+ probe lands, the question was not ASKED: UNJUDGED, naming the globs, never a FAIL against a repo
254
+ enforcing the rule on every file it means to.
255
+
256
+ Measured read-only. dielime: `2 directory(ies) it never names` becomes five OK lines, one per
257
+ registry, and nothing else in the run moves. during.day: the rules it enables only in `overrides[]`
258
+ were invisible to this check and are now counted — two configs move from silence to UNJUDGED, which
259
+ is what "we could not tell" has always meant here, and neither passes.
260
+
261
+ ### Patch Changes
262
+
263
+ - 915ac7c: **`drift`: a script that names a file that is not there (#74).** For every `scripts` entry in every
264
+ manifest, each file the script hands to `bun`, `node` or `tsx` must be on disk — during.day's
265
+ `bun run fast` began `bun scripts/check-plugin-version.ts && …`, the script was deleted, and the
266
+ 2.5 s gate had been failing at its first command ever since, found by a lucky grep. Only a word
267
+ carrying a file extension is asked about, so `bun test`, `bun run lint` and `node --version` name
268
+ subcommands, not paths.
269
+
270
+ **It also answers `--print-config-shape` (#75)**, naming every file it reads and the blocks it reads out of them.
271
+
272
+ **The law ceiling is only asked when the repo declares one (#79).** during.day read `1104 lines against a ceiling of 200` for a default it never chose — unactionable both ways. With no `law.maxLines` in `geonosis.json` the line is a SKIP that still prints the measured length, because that is the number a repo declares against.
273
+
274
+ **`runner`: a test script over zero test files is a SKIP (#71).** dielime's `packages/eslint-config` runs `vitest run` with no test file under it and got the exit-code warning — a line about a runner lying over a result that does not exist. The test-file walk is now one source shared with `drift`, so the two checks cannot disagree about the same tree.
275
+
276
+ **`drift`: the plugin line reads the user scope, and says what satisfies it (#72).** Both source repos were told "nothing here installs the kit's plugin" while it was enabled in `~/.claude/settings.json` for every repo on the machine. The user scope is now read and acknowledged, the WARN names the file and the `enabledPlugins` key that would satisfy it, and the check reads that key rather than the word `geonosis` appearing anywhere in the file.
277
+
278
+ **`drift`: the reverse direction asks the MANIFESTS (#78).** A `release` block whose package had been removed from every manifest, with a stale `node_modules` directory still on disk, resolved perfectly and the check said nothing. Whether a repo declared a dependency is a manifest question; whether a tool will load is resolution's, and the forward direction keeps it.
279
+
280
+ **`drift`: the no-block WARN is scoped to direct dependencies (#61).** `@geonosis/review` and `@geonosis/testbed` arrive transitively through `@geonosis/cli`, and both were WARNed about in a repo whose manifests name neither — a section asking a reader to configure something they did not choose is noise. A transitive presence is now an OK line naming where it came from, with no demand.
281
+
282
+ **`drift`: a workspace bin a script names must resolve from the root (#87).** pnpm links a workspace's bin only for the packages that depend on it, so dielime's `@dielime/gate-tools` had to become an explicit root devDependency before its gate scripts could find it — a debugging session to name a "command not found". Only bins this repo declares are asked about; an external command cannot be judged without knowing what the install put on disk.
283
+
284
+ **`drift`: NODE_OPTIONS around a counter that shells to pnpm (#63).** dielime's ts5 shim is preloaded in its `lint` script; the nested pnpm a counter spawns inherits the option, looks for a `.pnpmfile.mjs` that is not there, and dies. Both halves must be present for the line to fire, and it names the script and the counter.
285
+
286
+ **A tree with nothing in it to examine is refused, not passed.** Run in a scratch directory the doctor printed six SKIPs, three vacuous OKs and `doctor PASS` at exit 0 — from the tool whose thesis is that a SKIP is not a pass. With no `package.json` and no `.oxlintrc.json` anywhere under the root it now exits 2 with the sentence, the posture `geonosis-verify-arch` already takes over a config that names no packs.
287
+
288
+ **The usage is assembled from the check registry.** It said "Five questions" while seven checks shipped, and listed them in an order the registry does not use — a count and a list written by hand beside a registry, which is the failure this package is about, committed by its own help text. The descriptions are now a `Record<CheckName, …>`, so a check added without one does not compile, and it says what a no-argument run does.
289
+
290
+ **`drift`: a generated file names the gate that reads it back, and this asks whether anything runs it (#82).** `geonosis sync --write` left a 1,117-line byte-copy of during.day's law in AGENTS.md and nothing ever ran `sync --check`; the doctor stood next to it warning about the shorter original and said nothing about the copy. A file carrying `geonosis:gated-by: <gate>` is checked against every script, counter command and workflow. A file naming its WRITER and no gate fails on that alone — which is what during.day's own AGENTS.md does, so the recorded incident is caught as it stands.
291
+ - b9ad142: `resolveFrom` no longer answers from the doctor's own environment: pnpm's NODE_PATH (the hidden `.pnpm/node_modules` hoist) made Node resolve bare names from ANY directory, so drift said "installed" about repos that installed nothing (#118). Now a node_modules on the way up from the asked directory must carry the package before the resolver may finish.
292
+ - fe413db: **`--print-config-shape` names `overrides` (#102, second half).** The doctor started reading
293
+ `overrides[]` when it stopped taking a rule's base options for its whole configuration, and the
294
+ shape it prints — the contract that says which file it reads and which keys out of each — still
295
+ named `rules` alone. A tool reading a key it does not declare is a tool whose users cannot know what
296
+ it read: the same drift, one file over, in the tool that exists to catch it. A test holds every key
297
+ the config reader takes against the shape the bin prints.
298
+ - Updated dependencies [0a7673c]
299
+ - Updated dependencies [228566f]
300
+ - @geonosis/lint-parity@1.3.0
301
+
302
+ ## 1.2.0
303
+
304
+ ### Minor Changes
305
+
306
+ - 90eddee: `geonosis-doctor --only <check,…>` runs the named checks and no other — `--only loaded` in well under
307
+ a second, so declared-vs-loaded can live in a sub-second local gate. The doctor had subsumed a
308
+ consumer's own version check that lived in its 2.5 s gate; at eleven seconds the doctor could not
309
+ take its place there, and the check that catches a stale nested copy earliest ran only in CI — during
310
+ that very upgrade a stale copy appeared, caught by the script about to be deleted. A `loaded` FAIL
311
+ now says the remedy (remove the nested copies, reinstall); a check the doctor does not have is refused
312
+ naming the seven it does; the summary counts the checks that ran.
313
+
314
+ ### Patch Changes
315
+
316
+ - @geonosis/lint-parity@1.2.0
317
+
318
+ ## 1.1.0
319
+
320
+ ### Minor Changes
321
+
322
+ - 749239a: `geonosis-release deployed --check` compares `.geonosis/deployed.json` — what a pipeline reported
323
+ after promote — against the wrangler configs and secret names the tree declares. Drift either way
324
+ is exit 1; the file being absent is exit 2, never a pass. `@geonosis/doctor` gains a seventh line,
325
+ `deployed`, over the same file, reading it rather than importing the release package.
326
+ - 8464846: An option-gated rule can be exercised, and the `runner` line names the runner it read.
327
+
328
+ Both source repos adopted 1.0.0 and both filed the same two findings on 2026-08-30. Both were right
329
+ about the fact and wrong about whose defect it was.
330
+
331
+ **Probes.** A rule whose REACH is an option cannot fire on a fixed corpus: the shipped fixtures say
332
+ `acme` and `corpus/workflows/`, a repo says its own brand and `packages/workflows/src/`, and the
333
+ rule reads as firing nowhere in a tree that enforces it on every file. Each of the twenty
334
+ `requireOption` rules now declares a `probe(options)` beside its `fixShape` — the smallest source
335
+ that must fire it under the options it was handed, at a path that lands where it looks. The
336
+ doctor's `exercised` check writes that probe into a copy of the corpus and runs it again, and says
337
+ where the evidence came from. The PATH is half of it: `step-opens-its-own-cell` was silent because
338
+ its fixture sits at `corpus/workflows/` while the option says `packages/workflows/src/` — the same
339
+ bytes at the second path fire — so a probe derives its path from the option, never from a fixed
340
+ corpus location.
341
+
342
+ It does not soften the check. A probe that was PLACED under this repo's options and fired nothing is
343
+ a FAIL naming the option values. A rule with no probe, or one whose options admit no constructible
344
+ path, is a new fifth verdict — **UNJUDGED**: the question could not be ASKED. It does not pass (the
345
+ run still exits 1) and it does not accuse a repo enforcing the rule on every file, which a FAIL
346
+ there does — one consumer names such a rule as live enforcement in its own law.
347
+ `no-unregistered-plugin-dir` reads its registry off disk and cannot be probed by one file, which it
348
+ says rather than widening. The probes are read off the plugin the config loads, so a plugin too old
349
+ to declare any behaves exactly as before.
350
+
351
+ **`bun test`.** Measured on bun 1.4.0: a planted failing assertion exits 1, so does an awaited one,
352
+ so does a throw in `afterAll`. Its exit code is a verdict, and a bun workspace is now OK naming bun
353
+ and the measurement. The WARN is for vitest, whose `@cloudflare/vitest-pool-workers` really did
354
+ exit 0 over failing suites for weeks — eight lines of one consumer's report blamed it over scripts
355
+ running bun, which is how a reader learns to skim the section and miss the true lines beside them.
356
+
357
+ ### Patch Changes
358
+
359
+ - @geonosis/lint-parity@1.1.0
360
+
361
+ ## 1.0.1
362
+
363
+ ### Patch Changes
364
+
365
+ - 1c2ee0b: **Every package ships its declarations and says where they are.** `@geonosis/visual-diff` and
366
+ `@geonosis/verify` emitted no `.d.ts` at all (TS7016 in a consumer: the comparator every visual test
367
+ calls became `any` at the one seam that decides whether a screenshot regressed), and no package
368
+ declared `types` — the nineteen that worked did so through the sibling-file fallback, which ends the
369
+ day `exports` gains a condition. Every export now carries a `types` condition beside `default`, every
370
+ manifest a `types` field, and `tooling/types.test.ts` holds all twenty-one to it after the build.
371
+
372
+ `geonosis-verify-arch` prints a check once when its label is its id ("direction, orphan", not
373
+ "direction direction").
374
+ - Updated dependencies [1c2ee0b]
375
+ - @geonosis/lint-parity@1.0.1
376
+
377
+ ## 1.0.0
378
+
379
+ ### Major Changes
380
+
381
+ - c575f44: Five counters, `tolerance`, and a baseline rewrite that only ever goes down.
382
+
383
+ - `disabledCiJobs` — lines of `if: false` across the workflow files (`dir`). A job switched off to
384
+ get a release through is debt exactly like a lint error: invisible, because the pipeline goes
385
+ green. A condition that merely mentions `false` is not one, and a repo with no workflows
386
+ directory reads 0 rather than refusing; a workflow that is there and cannot be read refuses.
387
+ - `bundleBytes` — one integer out of whatever the repo's sizing command printed (`command`,
388
+ `match`), separators and all. With `match` it takes the group that pattern names and refuses when
389
+ it matches nothing, rather than falling back to any integer and reporting the wrong number as
390
+ confidently as the right one.
391
+ - `fastTierMs` — `finishedAt − startedAt` from the gate report `geonosis-verify` wrote (`report`,
392
+ `tier`). It refuses a report of another tier: the full tier takes minutes, and timing it against
393
+ the fast tier's baseline would report a regression of the wrong thing.
394
+ - `testsWithoutRunner` — workspaces holding tests with no `test` script. Unmeasured and passing are
395
+ indistinguishable from outside, so the count of unmeasured things has to be a number of its own.
396
+ - `packagesWithoutTypecheck` — workspaces with no `typecheck` script (`script`).
397
+
398
+ `tolerance` is new on the verdict: the fraction of the baseline a number may drift UP before the
399
+ ratchet calls it growth. Bytes and milliseconds move on a dependency patch nobody chose, and a gate
400
+ that fails on +40 bytes is a gate that gets switched off. It forgives noise upward only — a shrink
401
+ of any size still lowers the baseline — and a `tolerance` that is not a non-negative number stops
402
+ the run naming the entry, because one that silently became `NaN` reads exactly like a counter that
403
+ can never grow.
404
+
405
+ **Fix:** a baseline rewrite now lowers only the numbers that SHRANK. Writing every measured key back
406
+ was a harmless no-op while `held` implied `now === baseline`. Tolerance breaks that implication, and
407
+ the blanket rewrite would have laundered growth-within-tolerance into the new floor on the first
408
+ unrelated win — a baseline going up on a pass.
409
+
410
+ `examples/dielime.ratchet.json` is dielime's Phase 3 config: 36 keys, one `testFailures` entry per
411
+ workspace. It keeps `--format=unix` and `expectFormat` on the two oxlint counters, which dielime's
412
+ own config drops — the summary cross-check can tell a misparse from a clean run only when the run
413
+ printed a summary, and a warnings-only run in an unreadable shape prints one that says nothing
414
+ about errors. Pinning the format is what closes that window.
415
+ - c418f91: The `geonosis` Claude Code plugin — the method as enforcement rather than advice.
416
+
417
+ `plugin/` ships five hooks, Node ≥ 22, zero dependencies, every tool resolved from the CONSUMER's
418
+ `node_modules/.bin` so a repo that has not adopted a tool is never blocked on a tool it lacks:
419
+
420
+ - **SessionStart** runs `geonosis-doctor` if it is installed and prints a one-line digest of
421
+ `.oxlintrc.json` — which plugins are loaded, how many architecture rules are at `error`. It never
422
+ fails a session.
423
+ - **PreToolUse / Bash** refuses `prettier` (law 4) and `--no-verify` (law 7), and hands a
424
+ `git commit` to `geonosis-ledger` — the message its `-m` flags compose, and whether the staged
425
+ diff delivers anything but comments. The detectors are the ledger's, never reimplemented here; a
426
+ repo without it gets a note and no opinion.
427
+ - **PreToolUse / Edit|Write|MultiEdit** refuses a write that ADDS `eslint-disable`,
428
+ `oxlint-disable`, `@ts-ignore` or `@ts-expect-error` (law 3), any write to `gate-baseline.json` or
429
+ under `.geonosis/` (D-007 — the scored agent never writes the scoreboard), and a lint config that
430
+ sets a rule to `"warn"`, `"off"`, `0` or `1` (law 2). It reads what the call would ADD, never what
431
+ it removes, so the edit that DELETES a suppression is not refused by the guard against them.
432
+ - **PostToolUse / Edit|Write** runs oxfmt on the file just written. It never blocks.
433
+ - **Stop** runs `geonosis-verify fast` and will not let the turn end on a red one, capped at five
434
+ blocks per session — an agent retrying the same broken thing five times has a problem it will not
435
+ solve on the sixth, so the sixth prints the report and lets go.
436
+
437
+ `.claude-plugin/marketplace.json` offers it from `./plugin`. `pnpm generate:skill` now writes the
438
+ plugin's copy of the mechanical skill, the engineer agent with the machine-specific path taken out,
439
+ and `plugin.json` carrying the kit's own version; staleness tests fail when any of the four drift.
440
+ - 5f09ddc: Every hook is driven end to end, as Claude Code drives it.
441
+
442
+ Each script is SPAWNED with real stdin JSON over a throwaway repo carrying a `geonosis.json`, a
443
+ `gate-baseline.json`, an `.oxlintrc.json` and a `node_modules/.bin` linked to the BUILT
444
+ `geonosis-verify` and `geonosis-ratchet`. A hook tested by calling its policy function proves the
445
+ policy; only a spawn proves the exit code, the JSON on stdout and the tool resolution — which is
446
+ the whole of what Claude Code actually reads.
447
+
448
+ The Stop hook is exercised against a real fast tier that really goes red: a law file one line over
449
+ its baseline, so the ratchet grows and the hook blocks with exit 2, the failing step and its tail on
450
+ stderr. Then the sixth block prints the report and exits 0; a green run clears the session's count;
451
+ `stop_hook_active` short-circuits; and a repo with a BACKLOG that did not regress is not blocked at
452
+ all — which is the whole adoption argument, tested rather than asserted.
453
+
454
+ The suite was falsified before it was trusted: making Stop exit 0 instead of 2, dropping the
455
+ delivery check, and raising the cap to 50 each turned exactly the tests that should have gone red,
456
+ red.
457
+ - 950d188: **`geonosis-doctor` gains a fifth check: `observability`.**
458
+
459
+ Two of the four lying instruments of `docs/rules-backlog.md` #42 are the same shape — a deploy job
460
+ that reported success having deployed nothing, and a `/health` that answered ok over an unreachable
461
+ database. Both leave a green build and a silent project, and neither is visible from inside the
462
+ process that is not reporting. So the doctor asks three things about the exporter named in
463
+ `geonosis.json`'s `observability` block:
464
+
465
+ - **Is one configured.** No `sink` is a FAIL. A `sink` of `memory`, `noop`, `none`, `null`,
466
+ `console` or `swallowing` is a WARN — correct in a dev tree, and in a deployed one it is the
467
+ instrument that cannot fail.
468
+ - **Can anything reach it.** A HEAD to `endpoint` with a three-second timeout. ANY HTTP answer is
469
+ reachable, because an ingest endpoint legitimately answers 404 or 405 to a HEAD; a refused
470
+ connection, an unresolvable name or a timeout is a FAIL. No endpoint named is a SKIP.
471
+ - **Has anything arrived lately.** Read from the `lastEventFile` the sink writes, against
472
+ `maxAgeSeconds` (3600 by default). Missing file, unreadable file, a record with no numeric `at`,
473
+ or an age past the window are each a FAIL; no `lastEventFile` configured is a SKIP that says so.
474
+
475
+ And a fourth line for the case both of those miss: a block with neither a `lastEventFile` to read
476
+ nor a `probe` command to name is a WARN, because nothing in that repo has ever established that a
477
+ report reaches the sink. The doctor **reports** the probe command; it never runs it.
478
+
479
+ **The doctor imports nothing of `@geonosis/observability`.** It parses `{ at, id, sink, release }`
480
+ off disk. A check that needed the library it checks cannot run in the tree where the library is
481
+ missing, which is the first case it exists to find — and the wall is planted in the plugin's
482
+ `dogfood.test.ts` beside the others.
483
+
484
+ The producer half is `withLastEvent(sink, record)` in `@geonosis/observability`: a decorator that
485
+ writes the record after a capture the sink accepted. The WRITER is injected, because a file is a
486
+ Node fact and the same record goes to a KV namespace on workerd. It writes nothing for a refusal
487
+ and nothing for a deduplicated repeat — a record for a capture that did not send is precisely the
488
+ lie the freshness line exists to catch. It writes one for a swallowing sink, which is why freshness
489
+ is a line and `--prove` is the proof.
490
+ - 89c6769: The ratchet's own layers are walled, and the walls have a door that is proved open.
491
+
492
+ The three ratchet layers named FILE paths only, so `packages/ratchet/src/index.ts` — the published
493
+ entry — sat in no layer and could import anything, the CLI included, with lint green. It is
494
+ `ratchet-api` now, and the formatter it shares with the CLI has moved into a `ratchet-report` layer
495
+ of its own rather than being counted as part of the CLI it is not.
496
+
497
+ The dogfood suite gains a second kind of plant. A wall that refused everything would be as useless
498
+ as one that refused nothing, so each layer's permitted import — `verify` reaching the lock through
499
+ `@geonosis/ratchet` — is planted and asserted to stay CLEAN. Without that, the next engineer widens
500
+ `mayImport` to get their work through and the wall goes with it.
501
+ - b2d915f: **Fix:** a counter's command no longer loses what it wrote to stderr.
502
+
503
+ `<command> 2>&1` binds the redirect to the LAST command of the string. `echo boom >&2; echo fine`
504
+ therefore merged nothing, and on a ZERO exit `execSync` hands back stdout alone — so a tool that
505
+ prints its findings to stderr and exits 0 read as a tool that found nothing. That is the exact shape
506
+ of a gate that has quietly stopped gating, and it is the one direction in which this kind of bug is
507
+ dangerous: the number comes back lower than the truth and the baseline banks it.
508
+
509
+ On a NON-ZERO exit the line survived, because the failure path concatenates both captured buffers —
510
+ but concatenation put every stderr line after every stdout line, moving each diagnostic away from
511
+ the line it was about, which is what the evidence tail on a regression is for.
512
+
513
+ The command now runs in a subshell, so the shell itself merges the streams in the order they were
514
+ written. Measured before and after: `echo boom >&2` on a zero exit read `""` and now reads `boom`;
515
+ `echo one; echo two >&2; echo three; exit 4` read `one three two` and now reads `one two three`.
516
+ - 48bce0c: **Fix:** only a counter that measures a quantity accepts a `tolerance`.
517
+
518
+ Any counter took one. `{"counter":"lawLineCount","tolerance":0.5}` over a two-line law with a
519
+ baseline of 2 printed `ratchet PASS` at three lines — a 50 % growth, waved through. The same edit is
520
+ `"tolerance": 100` on `oxlintErrors`: the gate switched off from the config file, with the run still
521
+ reporting green. That is law 2 — never downgrade a rule — wearing a friendlier name, and it was the
522
+ easiest edit in the repo to get past a reviewer.
523
+
524
+ `Counter` gains `tolerates?: true`, declared only by `bundleBytes` and `fastTierMs`. Whether a
525
+ number is a measured quantity or a count of findings is the counter's to know, not the config's.
526
+ Anything else now stops the run, naming the entry:
527
+
528
+ ```
529
+ geonosis-ratchet: "oxlintErrors" does not accept a tolerance — only a counter that measures a
530
+ quantity declares one
531
+ ```
532
+
533
+ The plugin's write guard gains its first **ask**. A write to `geonosis.ratchet.json` that introduces
534
+ or raises a `"tolerance"` is neither refused nor allowed: it goes in front of the human as
535
+ `permissionDecision: "ask"`. Widening a band is legitimate and is not the agent's call — a band
536
+ somebody chose and a band somebody's agent chose are not the same thing. A tolerance carried through
537
+ an edit unchanged is not a change and says nothing.
538
+ - 461233c: Docs for the two new things: `docs/plugin.md` and `packages/verify/README.md`.
539
+
540
+ `docs/plugin.md` covers installing the plugin from a local path and from the remote once it is
541
+ pushed, what each of the five hooks does and the reason it does it that way, the five-block cap,
542
+ which files the runner owns and why the agent may not write them, what to put in `geonosis.json`,
543
+ and — the section every README skips — what the plugin CANNOT do. A `fast` tier of `["true"]` passes
544
+ for ever; only `geonosis-ratchet --prove` shows a counter can go red.
545
+
546
+ `packages/verify/README.md` is the runner's own: `--prove` first, the config with no default tier,
547
+ the three exit codes and why a report that could not be written is exit 2, the report shape with the
548
+ `<tier>/<n>` id rule, and `--exclusive`.
549
+
550
+ `docs/README.md` indexes both. The ratchet README gains rows for the five new counters, a section on
551
+ `tolerance` and the shrank-only rewrite it forces, and the landmine that cost an afternoon: a
552
+ loader or shim named RELATIVELY in a counter's command resolves to nothing under `--prove`, whose
553
+ probes run in a scratch directory. Write it `$PWD/…` and it works in both.
554
+ - e48501b: `@geonosis/verify` — one gate runner, with a report a hook can read.
555
+
556
+ `geonosis-verify <tier>` runs the shell steps a repo declared under `verify` in `geonosis.json`,
557
+ serially, stopping at the first red, and writes `.geonosis/gate-report.json`:
558
+ `{ tier, startedAt, finishedAt, ok, steps: [{ id, command, ok, exitCode, ms, tail, skipped? }] }`.
559
+ A step whose text is another tier's name is that tier, inlined in place; the inlined steps keep the
560
+ tier that declared them, so `fast/2` is the same command whether the operator ran `fast` or ran the
561
+ `full` tier that inlines it.
562
+
563
+ There is no default tier and there never will be one: no config, no `verify` object, or an unknown
564
+ tier is refused by name with exit 2, and so is a green run whose report could not be written — a
565
+ gate that cannot record has not passed. A failed step is exit 1.
566
+
567
+ `--exclusive` takes the ratchet's machine-wide lock through `@geonosis/ratchet`'s published entry
568
+ (`--exclusive-timeout <seconds>` bounds the wait), so a verify and a ratchet on one laptop serialise
569
+ against each other rather than each against itself. `--prove` plants a tier with one passing step
570
+ and one that exits 3, runs the real binary over it in a temp directory, and checks the failure
571
+ reached the report — a runner that reports green is worth nothing until it has been watched going
572
+ red on purpose.
573
+
574
+ The kit walls the new package with its own `layer-walls` rule: `verify` may import `ratchet-api`
575
+ and nothing behind it, planted against in the dogfood suite like the other walls.
576
+ - 7a6b019: **A fifth check, and a corpus a shipped one cannot be.**
577
+
578
+ `drift` asks whether a gate that was set up is still running: a CI job switched off by a condition
579
+ that can never be true (FAIL), a test file with no `test` script in any workspace from it up to the
580
+ root (FAIL), a manifest directory the registry named by `no-unregistered-plugin-dir` never mentions
581
+ (FAIL), the law's measured length against the ceiling `geonosis.json` sets (WARN), a
582
+ `.claude/settings.json` that installs no plugin (WARN), and a `geonosis.json` block whose package is
583
+ not installed — or an installed package with no block (WARN). Measured read-only over both source
584
+ repos: dielime's law is 382 lines and during.day's is 1,104, and neither installs the plugin.
585
+
586
+ `exercised` now also reads the corpus a repo ships for **its own options**, named by `geonosis.json`
587
+ → `doctor.corpus`. Three rules cannot be answered any other way — `layer-walls` fires on this repo's
588
+ layer names, `no-brand-names` on its brands, `plugin-route-namespaced` on its package root, and the
589
+ shipped corpus says `acme` and `layers/core`. Both source repos were failing on exactly that, in
590
+ trees that enforce those rules correctly. A rule is exercised when it fires in either corpus; a
591
+ declared corpus that is not there is a FAIL, and a rule the repo's own manifest names that fires
592
+ nowhere in it is a FAIL. It is read for the config beside `geonosis.json` and no other.
593
+
594
+ The kit now ships its own `lint-corpus/` and passes `geonosis-doctor` for the first time.
595
+ - 40cc71c: **Two fixtures that turned on the tree's install state, and the door's flag split.**
596
+
597
+ `the command registry > answers 2 when the bin is not installed here` and `doctor drift > warns
598
+ about a block whose package is not installed here` both named `@geonosis/testbed` while that package
599
+ did not exist — green for a reason that was not the code, and red the day the testbed shipped. Both
600
+ now name `@geonosis/definitely-absent`, which can never be installed: the registry case goes through
601
+ `runBin` directly, and `checkDrift` takes the block→reader map as a parameter (`READERS` is
602
+ exported) so a test can name a package no base will ever have. A new case asserts the inverse the
603
+ old one could not: every package the registry fronts is a dependency of `@geonosis/cli`, so
604
+ `findBin` finds all twelve.
605
+
606
+ `geonosis observability` joins the registry as a **write** — a real sink is somebody else's project,
607
+ and `kind` is now "something outside this process a reviewer would want to have approved", not only
608
+ a tracked file. `@geonosis/testbed` and `@geonosis/observability` are dependencies of the CLI, so
609
+ `geonosis testbed` resolves.
610
+
611
+ `geonosis <cmd> --help` used to print the DOOR's usage and never run the bin: the whole line was
612
+ scanned for `--help`. A flag before the command is the door's, everything after it is the command's.
613
+
614
+ ### Patch Changes
615
+
616
+ - Updated dependencies [c575f44]
617
+ - Updated dependencies [c418f91]
618
+ - Updated dependencies [5f09ddc]
619
+ - Updated dependencies [89c6769]
620
+ - Updated dependencies [b2d915f]
621
+ - Updated dependencies [48bce0c]
622
+ - Updated dependencies [461233c]
623
+ - Updated dependencies [e48501b]
624
+ - @geonosis/lint-parity@1.0.0
625
+
626
+ ## 0.6.0
627
+
628
+ ### Minor Changes
629
+
630
+ - e446760: **New package `@geonosis/doctor` (bin `geonosis-doctor`).** Four questions a version bump is not
631
+ finished until something has asked. Each one is a way enforcement has reported green while measuring
632
+ nothing, in a real repo, for weeks.
633
+
634
+ - **`loaded` — declared ≠ loaded.** oxlint resolves a `jsPlugins` specifier from the CONFIG FILE's
635
+ directory, so a nested `apps/<ws>/node_modules/@scope/plugin` left behind by a per-workspace
636
+ install runs while every manifest and the lockfile say a different version. One consumer measured
637
+ a whole bug report against 0.3.0 on a repo pinned to 0.4.0. For every config in the tree naming a
638
+ `@geonosis/*` plugin, this resolves it from that config's own directory, imports it, reads
639
+ `meta.version`, and compares it to the version the nearest manifest declares — then lists every
640
+ copy **resolution** reaches, one question per workspace. Never a `find`: on pnpm the store keeps
641
+ every version ever installed, so counting directories answers 7 on a correct tree. A declared
642
+ range it cannot read is a FAIL, not a pass.
643
+ - **`exercised` — enabled ≠ exercised.** Every rule a config enables, run against the corpus the
644
+ loaded plugin ships, under that config's own options — because the options are usually where the
645
+ reach went. Reuses `@geonosis/lint-parity`'s `corpusOf`; a plugin shipping no corpus is a SKIP.
646
+ - **`baseline` — a number that may only shrink, against another ref.** A ratchet lowers its own
647
+ baseline when a number shrinks, so nothing inside one checkout can see a branch raise one back up.
648
+ `--baseline-against [ref]` compares `gate-baseline.json` at HEAD with the same file at a ref. It
649
+ is OFF unless asked for by name: it is the only check that runs git, and this tool gets pointed at
650
+ repos other people are working in.
651
+ - **`runner` — an exit code is not a verdict.** Workspaces whose `test` script runs vitest or
652
+ `bun test` without asking for a JSON report, and which no `testFailures` counter in report mode
653
+ covers. `@cloudflare/vitest-pool-workers` exited 0 over suites it had just reported as failing.
654
+
655
+ `--strict` promotes every WARN to a FAIL; `--json` prints the whole report for CI. Exits 1 on any
656
+ FAIL, 2 when the run could not be made at all.
657
+
658
+ The package depends on `@geonosis/lint-parity` and on **nothing from the rule plugin** — it asks
659
+ which plugin a config loads, and a copy in its own dependency tree would be one more copy for the
660
+ question to be wrong about. A `layer-walls` layer named `doctor` in the kit's own config holds that
661
+ wall, planted against in `dogfood.test.ts`.
662
+
663
+ **`@geonosis/lint-parity` exports `resolveOxlint(from)`** — the consumer's own oxlint, walking up
664
+ from a directory. It was already the answer the CLI needed; it is now the answer every tool that
665
+ spawns oxlint shares, rather than each keeping its own.
666
+
667
+ **Measured, and worth knowing before writing anything that reads oxlint's output:** on oxlint 1.80 a
668
+ JS plugin rule that throws from `create()` is reported ONCE PER FILE, and `--format=unix` renders
669
+ each of those as `:0:0: [Warning]` — no path, no rule id, no sentence. The reason survives only in
670
+ the default formatter. `geonosis-doctor` therefore asks a second time, over one trivial file, in the
671
+ format that answers. No rule changed in this release.
672
+
673
+ ### Patch Changes
674
+
675
+ - Updated dependencies [e446760]
676
+ - @geonosis/lint-parity@0.6.0
@@ -17,11 +17,25 @@ const newest = (dir) =>
17
17
  }, 0)
18
18
  : 0
19
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)
20
+ const dist = join(here, '..', 'dist')
21
+ // `newest` answers 0 for a directory that is NOT THERE and for one that is merely old, so the
22
+ // two conditions were one comparison and one sentence — and the staleness sentence was printed
23
+ // over trees that had never been built at all, blocking a turn in a fresh git worktree, which
24
+ // has no dist because dist is not committed (#357). Only the second is evidence of a mistake.
25
+ if (existsSync(src)) {
26
+ const built = newest(dist)
27
+ if (built === 0) {
28
+ process.stderr.write(
29
+ 'geonosis-doctor: this tree has never been built — run pnpm build. No dist here at all, which is what a fresh clone or a linked git worktree starts with; nothing is stale.\n',
30
+ )
31
+ process.exit(2)
32
+ }
33
+ if (newest(src) > built) {
34
+ process.stderr.write(
35
+ 'geonosis-doctor: dist is older than src — run pnpm build before trusting this bin.\n',
36
+ )
37
+ process.exit(2)
38
+ }
25
39
  }
26
40
 
27
41
  await import('../dist/doctor-cli.js')
@@ -976,10 +976,12 @@ var preloadAroundPnpm = (root, workspaces) => {
976
976
  )
977
977
  ];
978
978
  };
979
- var GATED_BY = /geonosis:gated-by:\s*(.+?)\s*(?:-->|\*\/|$)/m;
979
+ var QUOTED_MARKER = /geonosis:gated-by:\s*[`'"]/;
980
+ var GATED_BY = /geonosis:gated-by:\s*(.+?)\s*(?:-->|\*\/|`|$)/m;
980
981
  var GENERATED_BY = /Generated by\s+`([^`]+)`/m;
981
982
  var CAN_CARRY_A_MARKER = /\.(?:mdc?|markdown|ya?ml|toml|[cm]?[jt]sx?|json[c5]?|txt|sh|mjs|cjs)$/i;
982
983
  var HEAD_BYTES = 4096;
984
+ var NOT_EXPANDED_YET = /\$\{[^}]*\}|\{\{[^}]*\}\}/;
983
985
  var markerIn = (path) => {
984
986
  let handle;
985
987
  try {
@@ -991,10 +993,11 @@ var markerIn = (path) => {
991
993
  const buffer = Buffer.alloc(HEAD_BYTES);
992
994
  const read = readSync(handle, buffer, 0, HEAD_BYTES, 0);
993
995
  const head = buffer.toString("utf8", 0, read);
994
- const gate = GATED_BY.exec(head)?.[1];
995
- if (gate !== void 0) return { gate };
996
+ const gate = QUOTED_MARKER.test(head) ? void 0 : GATED_BY.exec(head)?.[1];
997
+ if (gate !== void 0) return NOT_EXPANDED_YET.test(gate) ? { writes: gate } : { gate };
996
998
  const writer = GENERATED_BY.exec(head)?.[1];
997
- return writer === void 0 ? void 0 : { writer };
999
+ if (writer === void 0) return void 0;
1000
+ return NOT_EXPANDED_YET.test(writer) ? { writes: writer } : { writer };
998
1001
  } catch {
999
1002
  return void 0;
1000
1003
  } finally {
@@ -1030,6 +1033,13 @@ var generatedFiles = (root, workspaces) => {
1030
1033
  const run = everythingRun(root, workspaces);
1031
1034
  return marked.map(({ marker, path }) => {
1032
1035
  const at = relativePath(root, path);
1036
+ if ("writes" in marker) {
1037
+ return finding4(
1038
+ at,
1039
+ "SKIP",
1040
+ `this file WRITES the marker rather than carrying one \u2014 the name it holds, \`${marker.writes}\`, is still a template, so it names no gate this repo could run and there is nothing here to hold against one. Whatever this file generates is judged where it lands`
1041
+ );
1042
+ }
1033
1043
  if (!("gate" in marker)) {
1034
1044
  return finding4(
1035
1045
  at,
@@ -1996,20 +2006,20 @@ var checkExams = ({
1996
2006
  for (const { from, name } of declaredBy(workspaces)) {
1997
2007
  const exams = examsShippedBy(from, name);
1998
2008
  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
- );
2009
+ for (const exam of exams) {
2010
+ const importing = tests.find((one) => one.source.includes(name) && one.source.includes(exam));
2011
+ found.push(
2012
+ importing === void 0 ? finding7(
2013
+ `${name} \u2192 ${exam}`,
2014
+ "WARN",
2015
+ `this tree declares ${name} and no test file imports ${exam}. A floor bump that broke it would read exactly like a green tier: the exam is the only thing that asks.`
2016
+ ) : finding7(
2017
+ `${name} \u2192 ${exam}`,
2018
+ "OK",
2019
+ `${relativePath(root, importing.path)} runs ${exam}`
2020
+ )
2021
+ );
2022
+ }
2013
2023
  }
2014
2024
  return found.length > 0 ? found : [
2015
2025
  finding7(
@@ -2038,6 +2048,7 @@ var installedPluginVersions = (home, name) => {
2038
2048
  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
2049
  };
2040
2050
  var checkClaudePlugin = ({
2051
+ candidate = false,
2041
2052
  home = homedir2(),
2042
2053
  name = PLUGIN_NAME,
2043
2054
  train
@@ -2056,6 +2067,16 @@ var checkClaudePlugin = ({
2056
2067
  }
2057
2068
  const drifted = installed.filter((one) => one !== train);
2058
2069
  if (drifted.length === 0) return [];
2070
+ if (candidate) {
2071
+ return [
2072
+ {
2073
+ check: "loaded",
2074
+ message: `the packages here are ${train}, a candidate the registry does not have, and the plugin is ${drifted.join(", ")} \u2014 the marketplace is where \`claude plugin update\` looks, so the plugin CANNOT follow a candidate and this is not drift. It becomes a real question the moment ${train} publishes`,
2075
+ subject: `${name} (Claude plugin)`,
2076
+ verdict: "OK"
2077
+ }
2078
+ ];
2079
+ }
2059
2080
  return [
2060
2081
  {
2061
2082
  check: "loaded",
@@ -2267,6 +2288,25 @@ var gitHooks = (root) => {
2267
2288
  );
2268
2289
  });
2269
2290
  };
2291
+ var OFF_REGISTRY = /^(?:file:|link:|portal:)|\.tgz$/;
2292
+ var installedFromCandidate = (workspaces) => (
2293
+ // A repo that BUILDS the kit is at an unpublished version for the whole window between
2294
+ // `changeset version` and `changeset publish`, and has no override to be recognised by — the same
2295
+ // unactionable question for the same reason, one layer in (#354).
2296
+ workspaces.some((one) => (one.manifest.name ?? "").startsWith(SCOPE2)) || workspaces.some((one) => {
2297
+ const manifest = one.manifest;
2298
+ const blocks2 = [
2299
+ manifest["overrides"],
2300
+ manifest["resolutions"],
2301
+ manifest["pnpm"]?.["overrides"]
2302
+ ];
2303
+ return blocks2.some(
2304
+ (block) => typeof block === "object" && block !== null && Object.entries(block).some(
2305
+ ([name, spec]) => name.startsWith(SCOPE2) && typeof spec === "string" && OFF_REGISTRY.test(spec)
2306
+ )
2307
+ );
2308
+ })
2309
+ );
2270
2310
  var checkLoaded = async ({
2271
2311
  configs,
2272
2312
  home,
@@ -2288,7 +2328,13 @@ var checkLoaded = async ({
2288
2328
  for (const specifier of [...specifiers].toSorted()) {
2289
2329
  findings.push(await copiesOf({ root, specifier, workspaces }));
2290
2330
  }
2291
- findings.push(...checkClaudePlugin({ home, train: await trainVersion(root, [...specifiers]) }));
2331
+ findings.push(
2332
+ ...checkClaudePlugin({
2333
+ candidate: installedFromCandidate(workspaces),
2334
+ home,
2335
+ train: await trainVersion(root, [...specifiers])
2336
+ })
2337
+ );
2292
2338
  return findings;
2293
2339
  };
2294
2340
  var trainVersion = async (root, specifiers) => {
@@ -4,7 +4,7 @@ import {
4
4
  formatDoctor,
5
5
  formatJson,
6
6
  runDoctor
7
- } from "./chunk-GWVSC7GZ.js";
7
+ } from "./chunk-E46SFQ2K.js";
8
8
 
9
9
  // src/doctor-cli.ts
10
10
  import { fstatSync, statSync } from "fs";
package/dist/index.d.ts CHANGED
@@ -293,7 +293,14 @@ declare const installedPluginVersions: (home: string, name: string) => string[];
293
293
  * runs the hooks of the one before, and a refusal added this release cannot fire. That is the
294
294
  * declared-versus-loaded question one surface out, and it is this check's to ask.
295
295
  */
296
- declare const checkClaudePlugin: ({ home, name, train, }: {
296
+ declare const checkClaudePlugin: ({ candidate, home, name, train, }: {
297
+ /**
298
+ * Whether the packages here are a RELEASE CANDIDATE the registry has never seen — installed from
299
+ * tarballs or a `file:` override. `claude plugin update` reads the marketplace, so it cannot
300
+ * reach such a version at all, and the drift WARN is then unavoidable and unactionable for the
301
+ * whole life of the candidate: the one window in which a consumer reads this tool hardest (#354).
302
+ */
303
+ candidate?: boolean;
297
304
  home?: string;
298
305
  name?: string;
299
306
  /** The version the package train is on here, or nothing when this tree declares none. */
package/dist/index.js CHANGED
@@ -48,7 +48,7 @@ import {
48
48
  resolveFrom,
49
49
  runDoctor,
50
50
  satisfies
51
- } from "./chunk-GWVSC7GZ.js";
51
+ } from "./chunk-E46SFQ2K.js";
52
52
  export {
53
53
  CHECKS,
54
54
  COMPOSITION_ROOT,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geonosis/doctor",
3
- "version": "2.3.0",
3
+ "version": "2.4.2",
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": [
@@ -31,14 +31,15 @@
31
31
  }
32
32
  },
33
33
  "files": [
34
+ "CHANGELOG.md",
34
35
  "bin",
35
36
  "dist"
36
37
  ],
37
38
  "dependencies": {
38
- "@geonosis/lint-parity": "2.3.0"
39
+ "@geonosis/lint-parity": "2.4.2"
39
40
  },
40
41
  "devDependencies": {
41
- "@geonosis/ratchet": "2.3.0"
42
+ "@geonosis/ratchet": "2.4.2"
42
43
  },
43
44
  "peerDependencies": {
44
45
  "oxlint": ">=1.77"