@geonosis/oxlint-plugin-biological-architecture 2.3.1 → 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.
Files changed (2) hide show
  1. package/CHANGELOG.md +903 -0
  2. package/package.json +3 -2
package/CHANGELOG.md ADDED
@@ -0,0 +1,903 @@
1
+ # @geonosis/oxlint-plugin-biological-architecture
2
+
3
+ ## 2.4.2
4
+
5
+ ## 2.4.1
6
+
7
+ ## 2.4.0
8
+
9
+ ## 2.3.1
10
+
11
+ ### Patch Changes
12
+
13
+ - 204d24c: `stepsOf(queries, { undos, bind, execution, budget })` turns a module of bound queries into steps whose compensations travel with them, read as plain data from an undo registry — the shape `@geonosis/db`'s `undosOf` hands over (#249). A name in `undos` is a write and is always a step; a name that is not is a read, and a read is a step only under `execution: 'durable'`, where the body is invoked from the top and a read that answered differently the second time would divert the replay. `bind` re-derives the callable set from the step's OWN context, because a durable instance hibernates between steps. A call made from inside another step's invoke, or where no run is in flight at all, passes through to the plain query (#260) — a step within a step is a second record of one unit of work, and under a durable platform the nested call has no run context to be recorded in. Four added names: `stepsOf`, `BoundQuery`, `StepsOptions`, `StepUndo`; 121 → 125 on the door, nothing removed. `durable-body-reads-through-steps` takes `stepProxies` (default `[]`), so a repo that reaches the outside world through the proxy is not told every one of its own steps is a free read.
14
+ - 9e2bc64: `ssot-no-inline-facts` accepts a literal first argument to `createWorkflow`, the kit's own workflow factory — a workflow's name is what a durable platform memoises by and what a run record is dispatched from, it is declared at the call, and there is no owner module to import it from. It read 34 of them as second copies of a fact on one clean migration (#264). The list is now an option, `declarationCallees`, which ADDS to the kit's own rather than replacing it, so a repo that names `defineFlow` does not lose `createStep` with it. No rule added or removed; the rule's schema gains one optional array.
15
+
16
+ ## 2.3.0
17
+
18
+ ### Minor Changes
19
+
20
+ - 3dac9a7: **`no-runtime-dead-affordance`: a control that renders and answers nothing (#5).** A control atom
21
+ with no `onClick`, no other `on…` handler, no `asChild`, no `type="submit"`/`"reset"`, no
22
+ `form`/`formAction` and no `disabled` looks pressable and does nothing; `type="button"` is the
23
+ default and arms nothing. It reads the same element→atom map `no-raw-html-atoms` takes, and the map
24
+ is EMPTY by default (law 6) — with none the rule judges nothing, so joining the `frontend` preset
25
+ (which `recommended` is) breaks no config on install. A spread is a prop bag the rule cannot read
26
+ and is left alone.
27
+ - 3dac9a7: **A path grant is enumerated, and what it exempts is counted (#139).** Measured from the option
28
+ table, the plugin has exactly three path GRANTS — `time-through-the-door`'s `exempt` and the
29
+ `allow` of `font-roles-only` and `tokens-only-colors`; `within`, `paths`, `roots` and `routers` are
30
+ scopes, not exemptions. Each now takes `exemptModules` beside its pattern: modules named in full,
31
+ matched by path or by the tail a repo writes them as, never as a pattern — the `boundary-time-is-primitive`
32
+ model. The patterns still work; what changed is that they are counted. Each such rule declares
33
+ `meta.grants` (`{ paths, modules }`), so the doctor keeps no list of another package's options, and
34
+ `geonosis-doctor`'s `exercised` line WARNs with the number of files the pattern exempts in THIS tree
35
+ beside the number the enumeration names.
36
+ - 3dac9a7: **`no-swallowed-catch`: a rejection on a declared fetch or refresh path is answered, never dropped
37
+ (#3).** An empty `.catch(() => {})`, an empty `catch {}` and a handler that only logs all turn a
38
+ failed refresh into a successful one for everything downstream; the fix is to write the failure
39
+ where the caller reads it, or rethrow. The surface is the `paths` option and it is EMPTY by default
40
+ (law 6): with no surface named the rule says nothing at all, so a repo that has not opted in carries
41
+ no findings and no refusal. It ships in its own `failure-paths` preset.
42
+ - 3dac9a7: **`no-generic-utility-package`: a package is named for what it does (#171).** A workspace whose
43
+ manifest name ends in `utils`, `common`, `helpers`, `shared` or `misc` is named for nothing, so
44
+ nothing can be refused entry and everything arrives — web helpers, a constants object, a time door.
45
+ The rule reads the name out of the package's own manifest and reports ONCE, at the entry, so a
46
+ generic package is one finding rather than one per file in it. `names` and `entries` are options;
47
+ the default anti-names are universal, never a repo's vocabulary. Preset `tooling`.
48
+ - 3dac9a7: **`list-take-without-order`: a page taken in no order (#21).** A `list*()` or `query.graph` call
49
+ carrying `take` and no `order` gets whatever rows the database finds cheapest, which is stable in
50
+ development and different under load — so `[0]` answers a question about the page, not about the
51
+ table, and the row a previous stage just wrote is not in it. Both spellings are read: the graph
52
+ call's nested `pagination`, and a module service's second argument. A spread the rule cannot read is
53
+ left alone. `callees` and `orderKeys` default to the framework's spellings. Preset `backend-medusa`.
54
+ - 3dac9a7: **`store-route-scopes-tenant-data` reads options, follows the read one hop, and accepts declared
55
+ evidence (#2).** The route surface, the customer-owned entities and the names that count as scoping
56
+ are `routes` / `entities` / `scopingCalls`, each defaulting to what the rule hardcoded; `evidenceCalls`
57
+ (default none) names the predicates that verify caller-supplied evidence — a reference plus an email —
58
+ so a public tracking route no longer has to move its read out of the file to stay quiet. A relative
59
+ sibling is followed one hop, with its comments blanked, so moving the read sideways no longer
60
+ silences the rule. The file's text scan became an AST read, which drops the loose `customer:` marker:
61
+ the rule is stricter there, not weaker.
62
+ - 3dac9a7: **`no-unregistered-plugin-dir` reaches a flat-file seam (#36).** A `roots` entry may now be written
63
+ `{ path, files: true }`, which makes each FILE under that root a registrant named by its own
64
+ basename — a seam whose providers are files beside their registry was out of scope by construction,
65
+ so a channel nothing reached read as coverage. A bare string root is unchanged and still ignores a
66
+ one-segment path, because a repo that did not ask for the flat reading must not get it.
67
+ - 3dac9a7: **`test-no-clock-polling-for-promise-flips`: a test that already holds a promise awaits it (#4).**
68
+ A `waitFor` whose callback is nothing but `expect` assertions, in a scope that created a promise and
69
+ proves it is one by awaiting or chaining it, polls the scheduler for a flip the promise decides
70
+ exactly — a pass that was slower than the microtask, or a timeout with no cause. `testGlobs` and
71
+ `pollers` default to `.test.`/`.spec.`/`__tests__` and `waitFor`, so the rule runs bare; a scope with
72
+ no promise in it, and a `waitFor` whose body clicks or types, are both left alone. Preset `testbed`.
73
+ - 3dac9a7: **`no-date-string-compare`: a timestamp ordered by its text (#20).** `String(row.created_at)` on a
74
+ Date is `"Fri Aug 30 2026 …"`, so a `localeCompare` or a `<` over it sorts by weekday name — stable,
75
+ plausible and wrong, with no symptom. The rule reports a `*_at`/`*At` field turned into text by
76
+ `String()` or a bare template and then compared; a raw timestamp under `<`, an equality, and a
77
+ template with copy in it are all left alone, because an ISO string does order lexicographically.
78
+ `fieldPattern` names which fields hold a time, defaulting to both spellings. Preset
79
+ `backend-workflows`.
80
+
81
+ ## 2.2.0
82
+
83
+ ### Minor Changes
84
+
85
+ - d15a654: **A rule names the floor its pattern belongs to, and the doctor says when it is not shipped (#290).**
86
+ `document-sagas-are-generic` and `documents-share-one-table` lint a pattern this kit ships no floor
87
+ for: the rule is the requirement and there is nothing to install behind it, so a second consumer
88
+ hand-writes the floor to satisfy the lint. A rule's `meta` carries `floor` — `{ name, pending }` —
89
+ and the doctor's `exercised` line says "names a pattern the kit does not ship yet (<floor>)" for a
90
+ pending one a config enables. RULES.md carries the generated table, so the inventory says so where
91
+ somebody reads a rule before enabling it, and the entry clears when the floor ships.
92
+
93
+ ## 2.1.0
94
+
95
+ ### Minor Changes
96
+
97
+ - c94d02e: **`mutating-step-declares-retry` reads a nested budget's limit (#191).** Under `retryKey: 'retry'` — how `@geonosis/workflows` spells it — a step declaring `retry: { limit: 0 }` PASSED: the rule evaluated the budget only when it was a bare number, so on the engine whose budget IS an object the "default spelled out" half of the rule never ran. It now reads the number one level down. `{ limit: 3 }` still says nothing, an object naming no limit still says nothing, and a limit that is a constant is still unevaluable. The key inside that object is the new `retryLimitKey` option, default `limit`, refused when emptied out. Corpus case: `engine/bad-nested-zero-retry.ts`, a finding under this build and silent under the last.
98
+ - 40710b3: **The plugin ships its rule docs (#235).** The tarball carried `corpus`, `dist` and a README, so the option shape of `door` / `within` / `wrapper` / `exempt` was readable only out of the minified bundle's `ACCEPTS` strings. `RULES.md` is now in the published files — asked of `npm pack`, not of the manifest — and it carries a generated line per rule naming every option that rule accepts, read from the rule's own JSON schema. `pnpm generate:rules-doc` writes it and the doc gate fails when the committed section is not what the rules generate.
99
+
100
+ ### Patch Changes
101
+
102
+ - e932127: **A rule that starts refusing a bare run is a break, and now has to say so.** At 2.0.0 twelve rules refused a bare run that had not refused one at 1.4.0; the major changeset named `door` alone, and the first consumer read under it had its WHOLE oxlint config refused over `queryStepCalls` — a rule nothing had told anyone needed an option. A `tooling/` check now reads the bare-run table at the last published group tag (in the module it lives in today and in the suite it lived in at 1.4.0 — a tag with neither is a refusal, never a pass) and requires every rule that started refusing since, and every preset that started carrying one, to be named in a `major` changeset for this package. `RULES.md`'s inventory gains a **Requires** column carrying each rule's option, held against the table by a test so it cannot go stale.
103
+
104
+ ## 2.0.0
105
+
106
+ ### Minor Changes
107
+
108
+ - d22ae0f: **New rule `durable-body-reads-through-steps`** (`backend-workflows`). A durable platform memoises a step by NAME and journals no reads, so a free port read that gates a branch takes the other branch on a replay: the traced case completes, crashes before the run closes, replays, re-reads the status the first invocation wrote, throws its refusal and unwinds — refunding a charge for an invoice that is still sent, with the record saying `compensated`. Inside a `createWorkflow` body the options mark durable, a call reaching one of the repo's `ports` outside a `createStep` is a finding. `ports` is required (D-031); `durableKey` / `durableValue` cover both `durable: true` and `execution: 'durable'`, and the factories are options. An inline body is invoked once and is exempt.
109
+ - 75eed83: **D-031 refined: a rule scoped to a TREE refuses at the first file it would judge, and names it (#205).** `document-sagas-are-generic` refused in every package the preset reached — during.day's `packages/db` among them, which holds no saga and about which the rule has nothing to say either way. It now says nothing over a file that declares no flow, refuses over the first one that does with the option AND the file in the message, and is silent outside a `within` a config did name. A refusal about a tree with nothing in it is noise a consumer learns to route around, which is D-031's own failure mode pointed the other way.
110
+ - 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:`.
111
+ - 6b5b3db: **BREAKING for a durable workflow: the definition declares its execution, and each door refuses the other's (#210).** `createWorkflow` hardcoded `execution: 'inline'` and `WorkflowOptions` had no way to say otherwise, while `durable-body-reads-through-steps` read a `durable`/`execution` key off the call — a declaration a consumer could not write without an excess-property error, so the engine and its own rule disagreed about what a durable workflow looks like. `WorkflowOptions.execution` (default `'inline'`) is carried on the definition; `executeDurable` refuses a workflow declared inline and `.run()` refuses one declared durable, both by name — a body written for one request is replayed by the durable door, and that is exactly the free read the rule exists for. **REACH CHANGED:** the rule's defaults are now `durableKey: 'execution'`, `durableValue: 'durable'`, the engine's own spelling; a repo whose factory says it another way names `durableKey`/`durableValue`. A durable workflow must add `{ execution: 'durable' }`.
112
+ - c6761f6: **`backend-workflows` carries the step rules that reach both engines.** `mutating-step-declares-retry` and `step-declares-compensation-or-none` are in it as well as in `backend-medusa` — the retry rule only since its key became an option — so a repo on `@geonosis/workflows` gets them from the preset that describes it. Neither leaves `backend-medusa`: taking a live rule out of the preset it was written for is a downgrade wearing a preset change. `unique-query-step-name` stays Medusa's alone, because the collision it reports is that framework's map last-winning. The list of rules that refuse a bare run is now held against the plugin's own rules by a test rather than by hand, which found seven refusals nobody had proved through the binary.
113
+ - 24e4ba4: **New rule `subpath-over-barrel`, and a `package-subpaths` preset for it.** A root import whose names ALL live under one of the package's declared subpaths is a finding naming the subpath to use instead: a subpath exists because its dependency footprint differs from the root's, so the root entry is the union of them all and a file that names it to reach one pays for every other. Asked for by a consumer with 64 such sites, who was about to forbid the root with `no-restricted-imports` — which cannot say which subpath to use. The package→subpath map is a required option (D-031); names spanning two subpaths are what the root is for and are exempt, as are type-only imports, which erase before any of the cost happens.
114
+ - 5de0fba: **New rule `boundary-time-is-primitive` (backlog #145).** A date-library type — `Date`, `Dayjs`, a `Temporal` member — in an exported parameter, return or shape hands every caller the library and its configuration, and no database row or wire message can carry one; time crosses a boundary as epoch milliseconds or an ISO date string. The types are the `types` option (required — enabled without it the rule refuses the run) and the exemption is `doors`: a NAMED, enumerable list of the modules allowed to say the library's type out loud, never a path pattern, so the count of doors a repo has is a number somebody can read. Only signatures are judged; a `Date` a module builds and keeps inside a function body is its own business.
115
+ - 5d9aadb: **One door, two rules, one path grammar (#204).** A repo writes its time module down once and enables both `time-through-the-door` and `boundary-time-is-primitive` with it — but `doors` compared a plain relative path while `door` took a regex over the filename, so the spelling that worked for one made the other report the door's OWN signatures. Both now read a door through `isDoor`: the exact path, a path the filename ends with (the specifier callers import it by is one), or a regex over the normalised filename.
116
+ - 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.
117
+ - 8349abc: **`time-through-the-door` polices the library the repo names.** `dayjs` was compiled in, so a repo on date-fns or luxon got only the raw-`Date` half of the rule — and lost the half that catches a SECOND configured formatter, which is the defect that half exists for. `libraries` defaults to `['dayjs']`, so nothing changes for a repo that says nothing, and the diagnostic and the probe both name the library that repo declared. Non-breaking.
118
+ - abd2f2d: **REACH CHANGED: `time-through-the-door`'s `exempt` defaults to the test paths — `__tests__/`, `.test.`, `.spec.` (#203).** 1.4.0 had `__tests__` compiled into the rule; making every path an option emptied it, so the rule's reach GREW into test files nobody had asked it to read — during.day's first RC run reported six findings there. Test paths are not consumer vocabulary the way a door is. A repo that names its own `exempt` list replaces the default, and gets exactly the list it wrote.
119
+ - 38889b1: **`step-opens-its-own-cell` reads the scope where the engine declares it.** The rule's premise was a consumer's `step` wrapper, which only a repo without a scope seam needs. `@geonosis/workflows` has one — `scope.perStep` on the runtime a durable host builds — so the finding is now a host (`createRuntime`, `entrypointFor`, both configurable through `hosts`) that declares none, read anywhere inside the call so the adapter's nested runtime counts, and not judged at all when the config cannot be read whole. `scopeKey` names the key. `engine` and `wrapper` keep the `sagaflow-js` spelling reachable and are OFF unless set; `within` is what the rule now refuses without.
120
+ - adebb0c: **`document-sagas-are-generic` reads both engines' factories.** The callee was `saga` alone, so a per-entity `createWorkflow('invoice.create', …)` produced no finding at all — measured through the binary in 026-W2. `factories` defaults to `['saga', 'createWorkflow']` and takes the repo's own names. A flow whose name is ASSEMBLED from the entity is a template literal the rule cannot read, and that is deliberate: it is what the generic mint the rule sends people to looks like, so the remedy can never be the finding.
121
+ - 6883b18: **BREAKING (one release of grace): the preset `backend-sagaflow-cf` is now `backend-workflows-cf` (#197).** It was named after `sagaflow-js`, which the org's own engine replaced; the fact it carries — `step-opens-its-own-cell` — is about a durable run on Cloudflare Workflows, not about one library. A config naming the old preset resolves to the same rule set and says so once per process, whatever door read it; the old name goes at the next major. The rule stays OUT of `backend-workflows`: its defaults are the org engine's spelling (`createRuntime`/`entrypointFor`, `scope.perStep`), a Medusa repo has no `createRuntime`, and a rule enabled where it can find nothing reads as compliance — which is what the engine-presumption line (#159) exists to flag.
122
+ - 3e586a5: **`unique-query-step-name` names its query steps or refuses the run.** `queryStepCalls` loses its default: the two names it shipped with are Medusa framework calls with no counterpart, so on `@geonosis/workflows` the rule had zero reach while reading as enforced — and that engine has no query-step primitive to inherit instead, since a repeated step name there is defined rather than lost (`reserve`, `reserve#2` in call order). Enabled bare it now refuses and names the option, D-031's shape. Breaking for anyone who had it enabled: add `queryStepCalls: ["useQueryGraphStep", "useRemoteQueryStep"]`.
123
+ - 2fb94d5: **`mutating-step-declares-retry` reads the key the repo's engine writes.** The budget's key is a `retryKey` option, default `maxRetries` (Medusa's); `@geonosis/workflows` spells the same budget `retry: { limit, backoff }`, and with the key compiled in a correctly budgeted step on that engine was a finding for ever. Every diagnostic names the configured key, so the remedy is the one this repo writes. A nested budget is one the rule does not evaluate, like a constant or a spread. Non-breaking: a repo that says nothing keeps `maxRetries`.
124
+ - 7533b00: **`document-sagas-are-generic` guards the tree the repo names.** `/packages/workflows/src/` was compiled in, so the same per-kind saga one directory over was silent and no other layout could enable the rule at all. The tree is a `within` option — a regex over the normalised filename, like every other path option here — and enabled without it the rule refuses the run and names it, including when only `perKindPrefixes` was given. The probe is built back out of `within`, so the rule is exercisable under a repo's own layout. Breaking for anyone who had it enabled: add `within`.
125
+ - 1499635: **`worker-handles-are-scoped` names its wrapper and its tree.** `withCells` and `/apps/api/src/` were compiled in, so the rule served exactly one repo's layout and one repo's vocabulary — a scope opener called anything else (`withConnection`) read as unscoped for ever, and a cell worker one directory over read as clean. Both are options now, `wrapper` (a list, since a repo may open more than one kind of handle) and `within` (a regex over the normalised filename); the diagnostic names the configured wrapper, and the probe is built back out of `within`. Breaking for anyone who had it enabled: add both.
126
+ - b979c78: **`time-through-the-door` stops naming one repo's module.** The door is a `door` option (a regex over the normalised filename) and the four exemption directories that were compiled in are an `exempt` list with no default; the diagnostic names the configured module rather than `utils/time`. Enabled with no `door` the rule refuses the run and names the option, D-031's shape, so a repo that half-configures it cannot read as compliance. The rule declares a `probe(options)` that plants outside the configured door — checked against `door` and `exempt` rather than assumed clear — so `geonosis-ratchet --prove` and the doctor's `exercised` line hold under the new shape. Breaking for anyone who had it enabled: add `door`.
127
+ - 97bc8e9: **A debt marker names the plan that owns it.** New rule `todo-names-its-plan` (preset `tooling`): a marker must cite a plan — `TODO(025):` — or it is work nobody scheduled. The marker words are an option and the plan-id shape is an option, both empty-by-default, and enabled with no markers the rule refuses the run. Comments come from the PARSER, so the word in a string literal or after the `//` of a URL is not a finding. New counter `orphanTodos` counts what a repo already has — markers naming no plan, and markers naming a plan that is not on disk — so the law can be adopted as a ratchet rather than as a sweep. The kit enables the rule on itself; every example in the rule's own prose cites a real plan, because exempting a rule's source from the rule is the exemption that never comes back.
128
+
129
+ ### Patch Changes
130
+
131
+ - 36eafc4: **The plugin's atom discovery names its scratch directory `geonosis-atoms-` (#201).** One glob has to reach every directory the kit makes: 32,032 stale ones took a machine to `ENOSPC` on 2026-09-02, and 6,145 of the 10,502 still there an hour later — `atoms-`, `bio-rules-`, `parity-`, `unregistered-` — were outside the sweep everyone runs.
132
+ - e0aa82f: **`durable-body-reads-through-steps` says what it cannot see (#212).** It reads a call on a name `ports` lists, so a port read moved one function out — `readStatus(id)`, with `store.read` inside it — is invisible, and no rule that reads one file can follow it. The limit is in the fixShape now, with the way out (name the helper in `ports`, or make it the step the run needs anyway), and a corpus fixture holds the evasion so the silence is MEASURED rather than assumed.
133
+ - c1ee50d: **D-050's time discipline reaches the generated skill.** The two time rules' `fixShape` now carry the whole of it — the three kinds never mixed (an instant is epoch milliseconds, a business date is `YYYY-MM-DD` parsed strictly in UTC, display is a rendering produced at the edge), the clock taken as a `nowMs` parameter rather than read where it is needed, and the primitive as the thing that crosses a boundary. D-055 struck `@geonosis/time`, so the rules' own text is the only place the discipline ships; `skills/geonosis-code/SKILL.md` is regenerated and a test fails if a fixShape stops saying it.
134
+
135
+ ## 1.4.0
136
+
137
+ ### Minor Changes
138
+
139
+ - d5ee3d1: **`no-unregistered-plugin-dir` reads a SPLIT registry (#123).** `registry` now takes `string | string[]`. A consumer whose `medusa-config.ts` imports `medusa-config.fulfillment.ts` registers its carrier directories in the imported half, and reading only the named file made two REGISTERED directories a standing FAIL there. The halves are listed rather than followed — chasing imports out of a registry would need the repo's module resolution inside a linter — and one unreadable half still buys the silence a single unreadable file always did. The diagnostic names every half. Refusals from this rule now name the accepted shape as well as the missing key (#75), because a repo that reached for a list got `should be string` back and could not tell from the refusal whether a list was a shape the rule had.
140
+
141
+ ### Patch Changes
142
+
143
+ - 1a2f36c: **The reach floor is now proven it can fail (#124).** The corpus test that demands at least one firing file per rule had never been seen red, which is the same thing a counter with no probe is: a gate nobody has watched measure. A planted hole — the one directory that fires `no-unregistered-plugin-dir`, deleted from a corpus copy — must make the floor name that rule and no other, and the complete corpus must not name it at all.
144
+
145
+ ## 1.3.0
146
+
147
+ ### Minor Changes
148
+
149
+ - c9b63c0: **A probe declares FILES, not a file (#101).** A rule whose reach is an option declares
150
+ `probe(options)` so the doctor can exercise it under the repo's OWN values. One file was not enough
151
+ for a rule that reads a SIBLING off disk: `no-unregistered-plugin-dir` returns silently when the
152
+ registry its options name is not there, so its probe fired nothing — and dielime, enforcing that
153
+ rule at error over four registries, read as a permanent `exercised` FAIL for a gate that was
154
+ working. Their call, that the rule stays and the doctor is expected-red, was right, and it was the
155
+ indictment.
156
+
157
+ `probe` may now answer `{ files: [{ path, source }, …] }`, and `exercised` mounts every file it
158
+ declares into the copy of the corpus. `no-unregistered-plugin-dir` declares the directory under
159
+ `roots` AND the registry at the configured path — a registry carrying no string literal at all, so
160
+ nothing in it can name the directory beside it. Its own test moved from "fires nothing, and says so"
161
+ to the one finding every other probe has to produce, through the real binary.
162
+
163
+ The bare `{ path, source }` is the one-file case of the same contract and is read unchanged. That is
164
+ not tidiness: the doctor reads probes off the plugin build a consumer has INSTALLED, and every build
165
+ published before this shape existed answers in the bare form — refusing it would turn every probe in
166
+ an older install into no probe at all, which reads as an accusation against the repo.
167
+
168
+ The FAIL branch — a probe PLACED under the repo's options that still fires nothing — is now held by
169
+ a planted plugin whose one rule declares a probe and reports nothing, ever. It was held by
170
+ `no-unregistered-plugin-dir`, so fixing that rule would have quietly deleted the coverage.
171
+ - 12a942f: **`emit-declares-attempts`: an options budget built by a call never passes silently (#84).** dielime
172
+ ran the rule at ERROR for eight commits reporting nothing — every site's budget came through
173
+ `retryableEvent(…)` and no `budgetHelpers` were declared, so the rule returned on the exact shape it
174
+ exists to judge. Now: with no helpers declared, a call-built budget reports `undeclaredHelpers`
175
+ ("this repo declares no budget helpers — name them or pass `attempts` where it can be read"); with
176
+ helpers declared, a call the rule cannot even name reports `opaqueBudget`. A repo whose every emit
177
+ goes through a declared helper is unchanged. This is a judgement change and the release check claims
178
+ it (`--expect-changed emit-declares-attempts`).
179
+
180
+ ## 1.2.0
181
+
182
+ ## 1.1.0
183
+
184
+ ### Minor Changes
185
+
186
+ - 8464846: An option-gated rule can be exercised, and the `runner` line names the runner it read.
187
+
188
+ Both source repos adopted 1.0.0 and both filed the same two findings on 2026-08-30. Both were right
189
+ about the fact and wrong about whose defect it was.
190
+
191
+ **Probes.** A rule whose REACH is an option cannot fire on a fixed corpus: the shipped fixtures say
192
+ `acme` and `corpus/workflows/`, a repo says its own brand and `packages/workflows/src/`, and the
193
+ rule reads as firing nowhere in a tree that enforces it on every file. Each of the twenty
194
+ `requireOption` rules now declares a `probe(options)` beside its `fixShape` — the smallest source
195
+ that must fire it under the options it was handed, at a path that lands where it looks. The
196
+ doctor's `exercised` check writes that probe into a copy of the corpus and runs it again, and says
197
+ where the evidence came from. The PATH is half of it: `step-opens-its-own-cell` was silent because
198
+ its fixture sits at `corpus/workflows/` while the option says `packages/workflows/src/` — the same
199
+ bytes at the second path fire — so a probe derives its path from the option, never from a fixed
200
+ corpus location.
201
+
202
+ It does not soften the check. A probe that was PLACED under this repo's options and fired nothing is
203
+ a FAIL naming the option values. A rule with no probe, or one whose options admit no constructible
204
+ path, is a new fifth verdict — **UNJUDGED**: the question could not be ASKED. It does not pass (the
205
+ run still exits 1) and it does not accuse a repo enforcing the rule on every file, which a FAIL
206
+ there does — one consumer names such a rule as live enforcement in its own law.
207
+ `no-unregistered-plugin-dir` reads its registry off disk and cannot be probed by one file, which it
208
+ says rather than widening. The probes are read off the plugin the config loads, so a plugin too old
209
+ to declare any behaves exactly as before.
210
+
211
+ **`bun test`.** Measured on bun 1.4.0: a planted failing assertion exits 1, so does an awaited one,
212
+ so does a throw in `afterAll`. Its exit code is a verdict, and a bun workspace is now OK naming bun
213
+ and the measurement. The WARN is for vitest, whose `@cloudflare/vitest-pool-workers` really did
214
+ exit 0 over failing suites for weeks — eight lines of one consumer's report blamed it over scripts
215
+ running bun, which is how a reader learns to skim the section and miss the true lines beside them.
216
+
217
+ ## 1.0.1
218
+
219
+ ### Patch Changes
220
+
221
+ - 1c2ee0b: **Every package ships its declarations and says where they are.** `@geonosis/visual-diff` and
222
+ `@geonosis/verify` emitted no `.d.ts` at all (TS7016 in a consumer: the comparator every visual test
223
+ calls became `any` at the one seam that decides whether a screenshot regressed), and no package
224
+ declared `types` — the nineteen that worked did so through the sibling-file fallback, which ends the
225
+ day `exports` gains a condition. Every export now carries a `types` condition beside `default`, every
226
+ manifest a `types` field, and `tooling/types.test.ts` holds all twenty-one to it after the build.
227
+
228
+ `geonosis-verify-arch` prints a check once when its label is its id ("direction, orphan", not
229
+ "direction direction").
230
+
231
+ ## 1.0.0
232
+
233
+ ### Major Changes
234
+
235
+ - 94a2376: **`backend-medusa` grows from two rules to nine** — Medusa's own silences, turned into gates. Every
236
+ one exists because the framework INFERS something instead of making you say it. None throws; all are
237
+ found in production, far from the cause.
238
+
239
+ - **`mutating-step-declares-retry`** — `maxRetries` defaults to 0, so a step that writes and
240
+ declares no retry class reverts real state on the first transient failure. Read-only steps are
241
+ exempt; a budget the rule cannot evaluate passes.
242
+ - **`step-declares-compensation-or-none`** — `noCompensation = !compensateFn` is inferred from an
243
+ absent third argument, so a deliberate non-compensation and a forgotten one are the same source.
244
+ Write the function or declare `noCompensation: true`.
245
+ - **`named-when-block`** — an unnamed `when()` derives a `when-then-{ulid}` step name, so a run
246
+ resumed after a restart cannot find its checkpoint. The overloads are told apart by ARITY.
247
+ - **`subscriber-declares-id`** — the loader infers a subscriber id from a function or file name, and
248
+ two that infer alike dedupe each other with no error.
249
+ - **`no-loader-side-effect-in-index-file`** — `ResourceLoader` filters files named `index`, so a
250
+ resource declared in one registers only if something else happens to import it. Pure re-export
251
+ barrels pass. **Defaults to `subscribers/` and `jobs/` only**: measured in the installed 2.19,
252
+ `resource-loader.js:51` still filters and `workflows/workflow-loader.js:34` is the only caller in
253
+ the framework passing `allowIndex: true`, so `workflows/index.ts` IS loaded now.
254
+ - **`service-name-not-reserved`** — a module key matching Medusa's `Modules` enum silently REPLACES
255
+ the built-in at resolution. The 33 keys of 2.19 ship as the `reserved` option's default; the rule
256
+ follows `Module(SERVICE_NAME, …)` to the constant, declared before or after the call.
257
+ - **`no-index-count-as-exact`** — the index engine's count is an EXPLAIN plan-row estimate and reads
258
+ as an exact total to every consumer. Requires the label in the same payload.
259
+
260
+ Every framework name is an option with the framework's own value as its default — `stepFactories`,
261
+ `blocks`, `moduleFactories`, `resourceFactories`, `mutationPrefixes`, `estimateFields`, `labelKey`,
262
+ `paths`, `directories`, `reserved` — and each refuses the run when emptied out.
263
+ - c575f44: Five counters, `tolerance`, and a baseline rewrite that only ever goes down.
264
+
265
+ - `disabledCiJobs` — lines of `if: false` across the workflow files (`dir`). A job switched off to
266
+ get a release through is debt exactly like a lint error: invisible, because the pipeline goes
267
+ green. A condition that merely mentions `false` is not one, and a repo with no workflows
268
+ directory reads 0 rather than refusing; a workflow that is there and cannot be read refuses.
269
+ - `bundleBytes` — one integer out of whatever the repo's sizing command printed (`command`,
270
+ `match`), separators and all. With `match` it takes the group that pattern names and refuses when
271
+ it matches nothing, rather than falling back to any integer and reporting the wrong number as
272
+ confidently as the right one.
273
+ - `fastTierMs` — `finishedAt − startedAt` from the gate report `geonosis-verify` wrote (`report`,
274
+ `tier`). It refuses a report of another tier: the full tier takes minutes, and timing it against
275
+ the fast tier's baseline would report a regression of the wrong thing.
276
+ - `testsWithoutRunner` — workspaces holding tests with no `test` script. Unmeasured and passing are
277
+ indistinguishable from outside, so the count of unmeasured things has to be a number of its own.
278
+ - `packagesWithoutTypecheck` — workspaces with no `typecheck` script (`script`).
279
+
280
+ `tolerance` is new on the verdict: the fraction of the baseline a number may drift UP before the
281
+ ratchet calls it growth. Bytes and milliseconds move on a dependency patch nobody chose, and a gate
282
+ that fails on +40 bytes is a gate that gets switched off. It forgives noise upward only — a shrink
283
+ of any size still lowers the baseline — and a `tolerance` that is not a non-negative number stops
284
+ the run naming the entry, because one that silently became `NaN` reads exactly like a counter that
285
+ can never grow.
286
+
287
+ **Fix:** a baseline rewrite now lowers only the numbers that SHRANK. Writing every measured key back
288
+ was a harmless no-op while `held` implied `now === baseline`. Tolerance breaks that implication, and
289
+ the blanket rewrite would have laundered growth-within-tolerance into the new floor on the first
290
+ unrelated win — a baseline going up on a pass.
291
+
292
+ `examples/dielime.ratchet.json` is dielime's Phase 3 config: 36 keys, one `testFailures` entry per
293
+ workspace. It keeps `--format=unix` and `expectFormat` on the two oxlint counters, which dielime's
294
+ own config drops — the summary cross-check can tell a misparse from a clean run only when the run
295
+ printed a summary, and a warnings-only run in an unreadable shape prints one that says nothing
296
+ about errors. Pinning the format is what closes that window.
297
+ - c418f91: The `geonosis` Claude Code plugin — the method as enforcement rather than advice.
298
+
299
+ `plugin/` ships five hooks, Node ≥ 22, zero dependencies, every tool resolved from the CONSUMER's
300
+ `node_modules/.bin` so a repo that has not adopted a tool is never blocked on a tool it lacks:
301
+
302
+ - **SessionStart** runs `geonosis-doctor` if it is installed and prints a one-line digest of
303
+ `.oxlintrc.json` — which plugins are loaded, how many architecture rules are at `error`. It never
304
+ fails a session.
305
+ - **PreToolUse / Bash** refuses `prettier` (law 4) and `--no-verify` (law 7), and hands a
306
+ `git commit` to `geonosis-ledger` — the message its `-m` flags compose, and whether the staged
307
+ diff delivers anything but comments. The detectors are the ledger's, never reimplemented here; a
308
+ repo without it gets a note and no opinion.
309
+ - **PreToolUse / Edit|Write|MultiEdit** refuses a write that ADDS `eslint-disable`,
310
+ `oxlint-disable`, `@ts-ignore` or `@ts-expect-error` (law 3), any write to `gate-baseline.json` or
311
+ under `.geonosis/` (D-007 — the scored agent never writes the scoreboard), and a lint config that
312
+ sets a rule to `"warn"`, `"off"`, `0` or `1` (law 2). It reads what the call would ADD, never what
313
+ it removes, so the edit that DELETES a suppression is not refused by the guard against them.
314
+ - **PostToolUse / Edit|Write** runs oxfmt on the file just written. It never blocks.
315
+ - **Stop** runs `geonosis-verify fast` and will not let the turn end on a red one, capped at five
316
+ blocks per session — an agent retrying the same broken thing five times has a problem it will not
317
+ solve on the sixth, so the sixth prints the report and lets go.
318
+
319
+ `.claude-plugin/marketplace.json` offers it from `./plugin`. `pnpm generate:skill` now writes the
320
+ plugin's copy of the mechanical skill, the engineer agent with the machine-specific path taken out,
321
+ and `plugin.json` carrying the kit's own version; staleness tests fail when any of the four drift.
322
+ - 45407ec: **The generic articles as glob-scoped rule files, and a law you can start from.** Four
323
+ `.claude/rules/`-shaped files with `paths:` frontmatter, so three of them cost nothing until a
324
+ matching file is opened: `tiers.md` (`**/features/**`, `**/ui/src/**`), `gates.md` (`**` — the one
325
+ that loads with the law), `backend.md` (`**/workflows/**`, `**/api/**`, `**/db/**`) and `process.md`
326
+ (`plans/**`, `proofs/**`). The two area files are POINTERS at `geonosis-backend`, `geonosis-plan`
327
+ and `geonosis-process` rather than second copies of them.
328
+
329
+ `tiers.md` is **generated** from the `frontend` preset by `pnpm generate:skill`: the classification
330
+ cascade and the state matrix as prose, then every rule of the preset filed under the tier its name
331
+ is about, with the first sentence of its own description. It cannot name a rule the plugin does not
332
+ ship and cannot miss one it does, and a staleness test fails the build if the committed file is not
333
+ what the generator writes.
334
+
335
+ `docs/law-template.md` is the 45-line `CLAUDE.md` a repo copies: `@AGENTS.md` on the first line
336
+ (Claude Code does not read `AGENTS.md` natively, so the import is what makes one file serve every
337
+ other agent), the three presets as one-line choices — `state: controlled | zustand`,
338
+ `comments: rich | ceiling`, `tests: colocated | central` — the six laws, the gate, and a table
339
+ pointing at where the depth actually lives. Pinned at 60 lines by a test that measures the block a
340
+ repo copies, not the page explaining it.
341
+
342
+ **Measured, and worth saying: Claude Code has no `rules` plugin component.** A plugin contributes
343
+ skills, agents, hooks and MCP servers; `.claude/rules/` is a project directory and a plugin cannot
344
+ write into it. So the manifest lists the four under `metadata`, the free-form key — an inventory of
345
+ what the tree ships, never a wiring that loads them — and `plugin/rules/README.md` gives the one
346
+ symlink that installs them. A key nothing reads, in the shape of one that does, is the failure this
347
+ kit is about. `claude plugin validate ./plugin --strict` stays ✔ with the key.
348
+ - 5f09ddc: Every hook is driven end to end, as Claude Code drives it.
349
+
350
+ Each script is SPAWNED with real stdin JSON over a throwaway repo carrying a `geonosis.json`, a
351
+ `gate-baseline.json`, an `.oxlintrc.json` and a `node_modules/.bin` linked to the BUILT
352
+ `geonosis-verify` and `geonosis-ratchet`. A hook tested by calling its policy function proves the
353
+ policy; only a spawn proves the exit code, the JSON on stdout and the tool resolution — which is
354
+ the whole of what Claude Code actually reads.
355
+
356
+ The Stop hook is exercised against a real fast tier that really goes red: a law file one line over
357
+ its baseline, so the ratchet grows and the hook blocks with exit 2, the failing step and its tail on
358
+ stderr. Then the sixth block prints the report and exits 0; a green run clears the session's count;
359
+ `stop_hook_active` short-circuits; and a repo with a BACKLOG that did not regress is not blocked at
360
+ all — which is the whole adoption argument, tested rather than asserted.
361
+
362
+ The suite was falsified before it was trusted: making Stop exit 0 instead of 2, dropping the
363
+ delivery check, and raising the cap to 50 each turned exactly the tests that should have gone red,
364
+ red.
365
+ - 2732831: **The `/goal` command, in the plugin.** `plugin/commands/goal.md` unifies the two `/goal` commands
366
+ the source repos each grew separately: during.day's dispatch (`status`, `item <id>`, `rederive`), its
367
+ caps and its stop conditions, plus dielime's derive, value rubric, tiered verification and G4
368
+ meta-gate.
369
+
370
+ Boot is `geonosis-ledger status` and nothing else — never from memory, never from a summary. Pick is
371
+ **the pain the user actually stated**, above revenue path, operability, debt and capability; it is
372
+ the tier most often skipped because it is the least comfortable. Then EARS → RED → GREEN →
373
+ `geonosis-verify fast` → G2 review → G3 walk → proof → tick → commit, ledger row in the same commit
374
+ as the work it records.
375
+
376
+ **Where the two differed, the choice is recorded rather than averaged.** during.day's stop conditions
377
+ win over dielime's "NEVER STOP", which is a human directive to one repo and exactly what law 6 says
378
+ cannot be shared. dielime's `rederive` wins over `replenish`, because parallel read-only agents with
379
+ uncited candidates discarded is a mechanism and the other is a line. The caps are the kit's own
380
+ numbers — 2 revisions, 2 CI-fix attempts, 5 Stop blocks, draft PR — so the command does not put a
381
+ second bound on what the Stop hook already enforces.
382
+
383
+ **⚠AUTH reads `geonosis.json → caps.auth` (D-019), empty until a repo fills it.** during.day's list
384
+ is three specific backlog item numbers; hardcoding those would violate law 6.
385
+
386
+ **The G4 meta-gate carries all three rotation dimensions**, because each was earned by a failure that
387
+ passed every other gate: rotating *aspect* alone let five consecutive same-surface ticks through;
388
+ rotating *surface* alone let design rot for ten ticks; a consecutive counter alone was reset by one
389
+ off-aspect tick, so the distribution over the last six is checked too; and healthy rotation still
390
+ drifted to ever-smaller fixes, so there is an impact-tier floor.
391
+
392
+ `plugin/goal-command.test.ts` pins every `geonosis-*` command and every skill the file names against
393
+ what this workspace actually ships — a slash command naming a renamed tool fails halfway through a
394
+ tick with a shell error. Validated with `claude plugin validate . --strict` @ 2.1.251.
395
+ - 89c6769: The ratchet's own layers are walled, and the walls have a door that is proved open.
396
+
397
+ The three ratchet layers named FILE paths only, so `packages/ratchet/src/index.ts` — the published
398
+ entry — sat in no layer and could import anything, the CLI included, with lint green. It is
399
+ `ratchet-api` now, and the formatter it shares with the CLI has moved into a `ratchet-report` layer
400
+ of its own rather than being counted as part of the CLI it is not.
401
+
402
+ The dogfood suite gains a second kind of plant. A wall that refused everything would be as useless
403
+ as one that refused nothing, so each layer's permitted import — `verify` reaching the lock through
404
+ `@geonosis/ratchet` — is planted and asserted to stay CLEAN. Without that, the next engineer widens
405
+ `mayImport` to get their work through and the wall goes with it.
406
+ - b2d915f: **Fix:** a counter's command no longer loses what it wrote to stderr.
407
+
408
+ `<command> 2>&1` binds the redirect to the LAST command of the string. `echo boom >&2; echo fine`
409
+ therefore merged nothing, and on a ZERO exit `execSync` hands back stdout alone — so a tool that
410
+ prints its findings to stderr and exits 0 read as a tool that found nothing. That is the exact shape
411
+ of a gate that has quietly stopped gating, and it is the one direction in which this kind of bug is
412
+ dangerous: the number comes back lower than the truth and the baseline banks it.
413
+
414
+ On a NON-ZERO exit the line survived, because the failure path concatenates both captured buffers —
415
+ but concatenation put every stderr line after every stdout line, moving each diagnostic away from
416
+ the line it was about, which is what the evidence tail on a regression is for.
417
+
418
+ The command now runs in a subshell, so the shell itself merges the streams in the order they were
419
+ written. Measured before and after: `echo boom >&2` on a zero exit read `""` and now reads `boom`;
420
+ `echo one; echo two >&2; echo three; exit 4` read `one three two` and now reads `one two three`.
421
+ - ff9874b: `tokens-only-colors` and `font-roles-only`, in a new `themekit` preset.
422
+
423
+ A themed kit is only as themed as its least disciplined component, and the defect this catches is
424
+ the one nothing else does: a hardcoded colour or family keeps rendering, plausibly, in the brand it
425
+ was written for. Nothing throws, no page 404s, and a review sees a component that looks right.
426
+
427
+ `tokens-only-colors` fires on a colour written into a `className` or an inline `style` — hex,
428
+ `rgb()`, `hsl()`, `oklch()`, `lab()`, `color()`, and the 148 CSS named colours — including inside
429
+ the shorthands (`border: '1px solid #d8dadb'`, a `boxShadow`'s `rgba()`). It also fires on a `var()`
430
+ naming a custom property **outside** the configured token prefixes: `var(--legacy-blue)` is not a
431
+ literal and reaches around the theme exactly as completely as one. That second half is what makes
432
+ `tokenPrefix` load-bearing rather than decorative, and enabled without it the rule refuses the run
433
+ (D-031) — it cannot tell a token from a bypass, and a rule that cannot tell reads like a clean tree.
434
+
435
+ `font-roles-only` fires on a family literal and on a `var(--font-…)` naming a role the repo never
436
+ declared. Role names are that repo's data (law 6), so `roles` is required and the rule refuses
437
+ without it. It deliberately says **nothing** about bare `font-*` utility classes, because Tailwind
438
+ puts weight and style under the same prefix — a rule that fired on `font-bold` is a rule people
439
+ switch off, and the family violations go off with it.
440
+
441
+ Both take `allow` paths for the token layer, where a literal is the point. Both ship hand-built AST
442
+ cases and real fixtures through the real binary, including **an unedited component copied out of a
443
+ consumer's kit that passes** — a rule that has only ever run on source written to exercise it has
444
+ not been shown to leave real code alone.
445
+ - 063f928: Three defects in the two themekit rules, found by running them over both consumers.
446
+
447
+ Two false positives — a rule that fires on an accepted convention is a rule people switch off:
448
+
449
+ - **`font-[500]` is a WEIGHT, not a family.** Tailwind overloads `font-` in the arbitrary spelling
450
+ too, deciding family-or-weight by the value being a bare number. `font-roles-only` reported it in
451
+ three of a consumer's files. `font-[8bitoperator]` still fires — only a value that is *entirely* a
452
+ number is a weight.
453
+ - **A `var()` the element declares on itself is a local alias.** A component that writes
454
+ `style={{ '--pb-text': fromAProp }}` and reads `text-[var(--pb-text)]` back is not reaching around
455
+ the theme; the variable is set and consumed on one element and never leaves it.
456
+
457
+ And one **under**-reach that only the real binary could have shown, which is why law 8 requires both
458
+ test layers:
459
+
460
+ - **`style={{ … } as CSSProperties}` was invisible.** It parses as a `TSAsExpression`, and the style
461
+ reader only accepted a bare `ObjectExpression` — so every colour literal inside an asserted inline
462
+ style went unreported. That is how every repo writes a custom property in a style, because React's
463
+ `CSSProperties` has no index signature for one. The hand-built AST node was a bare object and
464
+ passed; the fixture through oxlint failed. Type assertions, `satisfies`, non-null and parentheses
465
+ are all unwrapped now.
466
+
467
+ Measured after the fixes: 219 findings across both consumer trees, read-only, in
468
+ `proofs/022-W6-consumers`.
469
+ - c3675c6: **`backend-medusa` grows from nine rules to twelve** — the per-file halves of three scans a consumer
470
+ runs repo-wide. Each was a fact about ONE file all along: one workflow body's steps, one route's own
471
+ path, one handler's own writes.
472
+
473
+ - **`unique-query-step-name`** (scan R4.3) — two `useQueryGraphStep`/`useRemoteQueryStep` calls in
474
+ one workflow body where either has no `.config({ name })`, or both have the same one. The
475
+ step-handler map is a plain assignment and the orchestrator appends a duplicate action with no
476
+ dedupe, so one of the two reads silently never happens and the workflow carries on with the
477
+ other's data. A SINGLE un-named step is exempt — it collides with nothing. A name held in a
478
+ constant passes: unreadable is not absent. Options `workflowFactories`, `queryStepCalls`.
479
+ - **`plugin-route-namespaced`** (scan R3.8's per-file half) — a route owned by a declared plugin
480
+ with no namespace segment under `/store` or `/admin`, or outside those scopes, or with a dynamic
481
+ `:param` where its namespace belongs. The routes-loader assigns last-write-wins, so a bare
482
+ `/store/<leaf>` is claimable by any other package with no error. `packages` mirrors the scanner's
483
+ `[{ under, name, namespaced }]` and is REQUIRED — an enabled rule with nobody declared a plugin
484
+ can never fire, and that reads exactly like compliance. An optional `namespace` per entry pins
485
+ WHICH segment it must be; left off, only presence is checked, because measured on a real tree
486
+ three of nineteen plugin namespaces deliberately differ from the directory name.
487
+ - **`route-no-inline-mutation`** (scan R5.1) — a service write in a route handler, which skips the
488
+ compensation and retry class behind the workflow door. Reads are exempt. The escape-hatch
489
+ annotation is read from the **statement**, and that is why this is an AST rule: the equivalent
490
+ text scan looks on the matching line or the one above, and a consumer's own annotated write is
491
+ spread over six lines by a cast, so the annotation lands five lines from the call and its gate is
492
+ red on a write a person already signed off. Options `paths`, `mutationPrefixes`,
493
+ `inlineMutationOk`.
494
+
495
+ `writesWith` — what counts as a service write — moves to `src/rules/lib/mutations.ts` so this rule
496
+ and `mutating-step-declares-retry` cannot drift into disagreeing about it, the same reason
497
+ `lib/steps.ts` exists. Proved inert: under `--expect-changed` over the corpus,
498
+ `mutating-step-declares-retry` has identical reach and an identical count on both sides.
499
+
500
+ Proved by the corpus rather than asserted: `--expect-changed` over the shipped corpus, old build and
501
+ old rule set against new, returns **HELD** for exactly these three and **no UNCLAIMED rule** — they
502
+ are the only behaviour change in this release.
503
+ - 48bce0c: **Fix:** only a counter that measures a quantity accepts a `tolerance`.
504
+
505
+ Any counter took one. `{"counter":"lawLineCount","tolerance":0.5}` over a two-line law with a
506
+ baseline of 2 printed `ratchet PASS` at three lines — a 50 % growth, waved through. The same edit is
507
+ `"tolerance": 100` on `oxlintErrors`: the gate switched off from the config file, with the run still
508
+ reporting green. That is law 2 — never downgrade a rule — wearing a friendlier name, and it was the
509
+ easiest edit in the repo to get past a reviewer.
510
+
511
+ `Counter` gains `tolerates?: true`, declared only by `bundleBytes` and `fastTierMs`. Whether a
512
+ number is a measured quantity or a count of findings is the counter's to know, not the config's.
513
+ Anything else now stops the run, naming the entry:
514
+
515
+ ```
516
+ geonosis-ratchet: "oxlintErrors" does not accept a tolerance — only a counter that measures a
517
+ quantity declares one
518
+ ```
519
+
520
+ The plugin's write guard gains its first **ask**. A write to `geonosis.ratchet.json` that introduces
521
+ or raises a `"tolerance"` is neither refused nor allowed: it goes in front of the human as
522
+ `permissionDecision: "ask"`. Widening a band is legitimate and is not the agent's call — a band
523
+ somebody chose and a band somebody's agent chose are not the same thing. A tolerance carried through
524
+ an edit unchanged is not a change and says nothing.
525
+ - 461233c: Docs for the two new things: `docs/plugin.md` and `packages/verify/README.md`.
526
+
527
+ `docs/plugin.md` covers installing the plugin from a local path and from the remote once it is
528
+ pushed, what each of the five hooks does and the reason it does it that way, the five-block cap,
529
+ which files the runner owns and why the agent may not write them, what to put in `geonosis.json`,
530
+ and — the section every README skips — what the plugin CANNOT do. A `fast` tier of `["true"]` passes
531
+ for ever; only `geonosis-ratchet --prove` shows a counter can go red.
532
+
533
+ `packages/verify/README.md` is the runner's own: `--prove` first, the config with no default tier,
534
+ the three exit codes and why a report that could not be written is exit 2, the report shape with the
535
+ `<tier>/<n>` id rule, and `--exclusive`.
536
+
537
+ `docs/README.md` indexes both. The ratchet README gains rows for the five new counters, a section on
538
+ `tolerance` and the shrank-only rewrite it forces, and the landmine that cost an afternoon: a
539
+ loader or shim named RELATIVELY in a counter's command resolves to nothing under `--prove`, whose
540
+ probes run in a scratch directory. Write it `$PWD/…` and it works in both.
541
+ - e48501b: `@geonosis/verify` — one gate runner, with a report a hook can read.
542
+
543
+ `geonosis-verify <tier>` runs the shell steps a repo declared under `verify` in `geonosis.json`,
544
+ serially, stopping at the first red, and writes `.geonosis/gate-report.json`:
545
+ `{ tier, startedAt, finishedAt, ok, steps: [{ id, command, ok, exitCode, ms, tail, skipped? }] }`.
546
+ A step whose text is another tier's name is that tier, inlined in place; the inlined steps keep the
547
+ tier that declared them, so `fast/2` is the same command whether the operator ran `fast` or ran the
548
+ `full` tier that inlines it.
549
+
550
+ There is no default tier and there never will be one: no config, no `verify` object, or an unknown
551
+ tier is refused by name with exit 2, and so is a green run whose report could not be written — a
552
+ gate that cannot record has not passed. A failed step is exit 1.
553
+
554
+ `--exclusive` takes the ratchet's machine-wide lock through `@geonosis/ratchet`'s published entry
555
+ (`--exclusive-timeout <seconds>` bounds the wait), so a verify and a ratchet on one laptop serialise
556
+ against each other rather than each against itself. `--prove` plants a tier with one passing step
557
+ and one that exits 3, runs the real binary over it in a temp directory, and checks the failure
558
+ reached the report — a runner that reports green is worth nothing until it has been watched going
559
+ red on purpose.
560
+
561
+ The kit walls the new package with its own `layer-walls` rule: `verify` may import `ratchet-api`
562
+ and nothing behind it, planted against in the dogfood suite like the other walls.
563
+ - 88d7bf6: **`@geonosis/cli` — one declarative command registry, and the `geonosis` bin over it.** Fifteen
564
+ commands: eleven that front a kit bin by SPAWNING it, and four that are the CLI's own. The registry
565
+ is the list every door reads — the MCP server serves these commands, not a second copy of them.
566
+
567
+ A wrapped command passes the child's exit code through unchanged. The nine bins agree that 0 is
568
+ clean, 1 is a finding or a refusal and 2 is a run that could not be made at all, and a wrapper that
569
+ normalised a 2 into a 1 would have invented a finding out of a tool that never ran. The bin is found
570
+ by resolution — the repo's copy first, this package's second, and it prints which one it ran when it
571
+ falls back, because unseen version skew is the failure `geonosis-doctor` exists to catch.
572
+
573
+ - `init` writes the four config files an existing repo needs and refuses to overwrite any of them —
574
+ and when one exists it writes none of the others. It will not enable a rule whose options only the
575
+ repo can answer, because an enabled rule without its options refuses the whole run at load
576
+ (D-031); which rules those are is MEASURED by constructing each rule with no options, never listed.
577
+ - `sync` generates `AGENTS.md` and `.cursor/rules/*.mdc` from `CLAUDE.md` and the repo's skills —
578
+ one law, every tool (D-002) — deterministically, so `--check` is a gate. `--target` is handed to
579
+ the ledger's edges table rather than read a second time.
580
+ - `discover` reads the findings a repo's own config already produces into `fixShape` candidates and
581
+ writes nothing.
582
+ - `skills list|lock|audit` pins skills by content hash in during.day's lock shape, and the audit
583
+ fails on drift, on a skill nobody pinned, and on an executable file inside a skill directory — the
584
+ supply-chain class a hash over `SKILL.md` is blind to.
585
+
586
+ The plugin gains a `cli` layer in the kit's own `.oxlintrc.json`: it may read the presets, the parity
587
+ tool and the doctor's resolver, and no gate it fronts. Three walls and three doors are planted
588
+ against in `dogfood.test.ts`.
589
+ - 2637a99: **`@geonosis/mcp` — the same registry, served over MCP.** `geonosis-mcp` is a stdio server whose
590
+ tools are `@geonosis/cli` commands and nothing else, so an agent and the operator's terminal cannot
591
+ disagree: a test connects an in-memory client to the real server and asserts the `doctor` tool's
592
+ bytes are the bytes of `geonosis --json doctor`.
593
+
594
+ - **Generated `instructions`**, Midday's mechanic: the law's measured line count, the rules the
595
+ repo's `.oxlintrc.json` actually enables (never the ones it set to `off`), the counters the
596
+ baseline holds, and the read/write kind of every tool. A repo missing one of those files is told
597
+ so rather than given an invented number.
598
+ - **The root is the server's.** No tool takes a directory, so no client can point a gate at another
599
+ tree and read the answer as this repo's.
600
+ - **A write is two calls**, dielime's Trench gate, kept stateless: the first answers with a PREVIEW
601
+ and a token that is the hash of the tool id and the arguments; the second must carry `confirm:
602
+ true` and that token. A token for different arguments is refused rather than run, and nothing is
603
+ remembered between calls, so one client's preview is never another's authority.
604
+
605
+ The plugin gains an `mcp` layer in the kit's own `.oxlintrc.json` — it may import `cli` and nothing
606
+ else the kit ships — planted against in `dogfood.test.ts`.
607
+ - 75f613e: **`create-geonosis` — a third repo that passes its own gates on day one.** `create-geonosis <dir>
608
+ --runtime pnpm|bun --backend medusa|sagaflow-cf|none --ui tiered-shadcn|atoms-only|none` writes a
609
+ workspace with one catalog of shared versions, an `exports` map and no barrel in every package,
610
+ `typecheck` + `lint` + `test` in EVERY workspace (each asking vitest for its own JSON report), a law
611
+ under sixty lines, the five config files, a lefthook, a CI workflow with nothing switched off in it,
612
+ an empty skills lock and a `.claude/settings.json` that installs the plugin.
613
+
614
+ There is no default for the three answers that decide the shape of the tree. A default borrowed from
615
+ dielime or during.day would be one repo's stack hardcoded into every repo this ever writes.
616
+
617
+ - **The edges table is the one place the graph is written.** `docs/architecture.md` carries it, and
618
+ the scaffold runs `geonosis-ledger sync --target turbo` and `--target layer-walls` over it, so
619
+ turbo's boundaries and the linter's layers are generated from the same table and cannot drift.
620
+ - **The baseline starts at zero, except where zero would be a lie**: `lawLineCount` starts at the
621
+ measured length of the `CLAUDE.md` just written.
622
+ - **`lint-corpus/`** is a reach corpus in the new repo's own vocabulary, for the three rules whose
623
+ reach is repo-specific — `layer-walls`, `no-brand-names`, `plugin-route-namespaced` — because the
624
+ plugin's shipped corpus says `acme` and `layers/core` and is no evidence about anybody else.
625
+
626
+ `@geonosis/cli`'s `rulesFor` now takes a map of pre-built rule options instead of a brand, so `init`
627
+ and the scaffold answer the same question in one place. The plugin gains a `create` layer, walled
628
+ against every gate it configures; planted in `dogfood.test.ts`.
629
+ - f60a869: **The six defects the 1.0.0 reviews filed, before the release: two of them about being installable
630
+ at all, three about refusing honestly, one about counting a finding twice.**
631
+
632
+ `geonosis-review` read its command line OUTSIDE `main`'s try, so `--definitely-not-a-flag` reached
633
+ node uncaught: a stack trace and exit **1** — the one code the contract reserves for "the review was
634
+ read and REFUSED", so a caller branching on the code read a typo as a rejection. Parsing happens
635
+ inside now, every command-line mistake is 2 on one line, and the README's exit codes are a table
636
+ that says which is which (#50).
637
+
638
+ `@geonosis/testbed`'s `bin` named `dist/cli.js`, the only bin in the kit that pointed at a build
639
+ output. `pnpm install` links bins BEFORE anything is built, so a fresh clone printed
640
+ `Failed to create bin … ENOENT` and had no `geonosis-testbed` — including the copy `@geonosis/cli`
641
+ spawns for its `testbed` door. It has a committed `bin/geonosis-testbed.mjs` shim like every other
642
+ package, and `tooling/bins.test.ts` holds every `bin` entry to a file git carries (#51).
643
+
644
+ Two shims — `create-geonosis` and `geonosis-mcp` — were **committed at mode 644**. pnpm sets the
645
+ executable bit on the link it creates, so the mode was right everywhere except where it is kept, and
646
+ the flip only ever surfaced as an unrelated diff in somebody else's commit. Both are 755, and the
647
+ test reads the mode **git records**, which is the mode a clone gets (#52). Measured on this base:
648
+ `doctor` and `verify-arch`, the two the review named, were already executable.
649
+
650
+ `geonosis-testbed matrix` crashed on a malformed `testbed.matrix`: a block spelling `majors` as
651
+ `postgres` reached the renderer, which read `.length` off `undefined` — a TypeError at exit 1 for a
652
+ config error one line long. `readDbMatrix` checks the block first and refuses with **2**, naming
653
+ every wrong key at once (a missing one and an unknown one are two findings, and being told one at a
654
+ time means editing the file twice). Hand-rolled, not Zod: this package has no runtime dependencies
655
+ and a test that keeps it that way (#45).
656
+
657
+ `no-mock-db-in-integration` reported an imported handle **twice at one position**: an
658
+ ImportSpecifier's `imported` and its `local` are two nodes over the same span when nothing is
659
+ renamed. One report per place now — the corpus carries the fixture that shows it, and a finding
660
+ counted twice is a finding a ratchet counter banks twice (#46).
661
+
662
+ `geonosis-ledger sync --write` refused a config that did not already carry the fragment
663
+ (`"boundaries": no such key`), so the command whose job is to write the config could not write the
664
+ first one and hand-authoring the generated shape was the way in. It creates the key now — in a
665
+ `turbo.json` with no `boundaries`, in a config that has never named `layer-walls`, in a package with
666
+ no `no-restricted-imports`, and the `rules` object itself if that is missing too — and still writes
667
+ only that fragment. A rule is grown as `["error", { … }]`, the severity being the one thing no
668
+ generator can read off the table (#34).
669
+ - 35adf9a: Two rules and two presets. `no-unregistered-plugin-dir` (preset `integrations`) reports a plugin
670
+ directory the registry names nowhere — Midday ships eight such directories, dielime ships zero, and
671
+ a seam whose providers are flat files is out of scope because nothing matches. `router-schema-parity`
672
+ (preset `backend-trpc`) reports a router with no schema and a schema with no router, with
673
+ repo-root-relative directories because during.day's two halves are in different packages, and a
674
+ `mounts` option because its three apparent gaps are one deliberate arrangement.
675
+ - 13bd54d: Two option changes both earned by running the rules over the consumers read-only.
676
+ `no-unregistered-plugin-dir`'s `manifests` is now required with no default: the default matched
677
+ nothing in dielime, whose plugin directories hold `service.ts` and no index, so the rule reported a
678
+ clean tree it had never looked at. `emit-declares-attempts` gains `budgetHelpers` (empty by default,
679
+ so nothing changes until configured): all 54 of dielime's emit sites spell the bag
680
+ `options: retryableEvent(…)`, the one shape the rule declined to judge, so it had zero coverage of
681
+ the workspace whose landmine it was written from.
682
+ - c479255: The database matrix as a rendered workflow, and the rule both consumers already pass.
683
+ `geonosis-testbed matrix` renders `testbed.matrix` from `geonosis.json` into two GitHub Actions jobs
684
+ — because the source repo's embedded leg is real, tested and invisible, running only where the
685
+ connection variable is unset. It refuses majors with an empty connection string, which would run the
686
+ embedded cell under a container's name and go green twice against one database. `geonosis-testbed
687
+ strict` is the second door onto the report-not-exit-code runner. New rule
688
+ `no-mock-db-in-integration` in the new `testbed` preset: `vi.mock`/`jest.mock`/`mock.module` of a
689
+ configured database module inside a configured integration directory, plus `mockNames` for the
690
+ handle-through-a-helper shape no mock call can be seen in. `integrationDirs` and `dbModules` are
691
+ required with no defaults, because dielime's 106 integration files and during.day's whole tree
692
+ contain zero mocked databases — they are the green fixtures, and a default drawn from them would
693
+ report a clean tree it had never looked at.
694
+
695
+ ## 0.6.0
696
+
697
+ ### Minor Changes
698
+
699
+ - **No rule changed.** The version moves because the packages release as one fixed group. Measured,
700
+ not assumed: `geonosis-lint-parity --corpus` 0.5.0 vs 0.6.0 under both consumers' own configs
701
+ reports the identical finding count for every in-scope rule on both sides (38 rules under
702
+ dielime's config, 24 under during.day's), and the tree parity is 135 = 135 and 0 = 0 with nothing
703
+ lost, gained or reworded. `--expect-changed atom-no-deps` comes back UNPROVEN, which is the same
704
+ statement from the other direction. `proofs/022-parity-0.5.0-vs-0.6.0-dielime/`.
705
+
706
+ - **The last step of every bump is now `npx geonosis-doctor`.** The README's hand-run
707
+ `node --input-type=module -e '… p.meta.version'` stays as the explanation of what the tool does;
708
+ the tool asks it for every config in the tree, from each config's own directory, and lists every
709
+ copy resolution reaches. See [`@geonosis/doctor`](https://www.npmjs.com/package/@geonosis/doctor).
710
+
711
+ ## 0.5.0
712
+
713
+ ### Minor Changes
714
+
715
+ - **New rule `cell-no-cross-feature-organelles` (preset `frontend`).** `organelle-dependency` is scoped
716
+ to `organelles/` and `no-cross-feature-stores` only judges stores, so a CELL importing another
717
+ feature's ORGANELLE passed both while both stayed green. Option `sharedFeatures`, default
718
+ `['shared']` — the same option and the same default as `organelle-dependency`'s, because the two
719
+ rules draw one boundary from two sides. Measured on a consumer: three findings in two files.
720
+
721
+ **The corpus carries both spellings of every direction rule.** A direction rule judges where an
722
+ import lands, so `@/features/billing/x` and `../../billing/x` are one breach; the corpus previously
723
+ carried whichever spelling each rule's author happened to write. Fifteen fixtures added. No rule
724
+ changed.
725
+
726
+ **`geonosis-lint-parity --corpus --expect-changed <rule,…>`.** A changelog line that names a rule is
727
+ a claim; this makes it data the corpus judges, and exits 1 on a claim the corpus is silent about or a
728
+ change nobody claimed. Mandatory for any release whose notes name a rule.
729
+
730
+ **`testFailures` reads the runner's JSON report** with `report: "vitest-json"`, refusing when the
731
+ report is absent, unparsable, missing `numFailedTests`, or says `success: false` while naming none.
732
+ `Counter.probe` takes a list, so `--prove` proves each reading mode by name.
733
+
734
+ **`geonosis-ratchet --prove` proves the lock**, by running two children of the CLI over a slow
735
+ `--hold` and requiring the second to start after the first finished.
736
+
737
+ `@geonosis/ratchet`'s source is layered — `core/`, `counters/`, `report.ts`, `cli.ts` — with no
738
+ change to what `@geonosis/ratchet` exports or to a single line the CLI prints.
739
+
740
+ ## 0.4.0
741
+
742
+ ### Minor Changes
743
+
744
+ - The rule core, and the one widening that came with it (plan 018, K1.2).
745
+
746
+ Measured before any of it existed: **37 of 54 rules normalised the filename themselves**, in three
747
+ spellings, one of which fixed only the first separator; **21 parsed an import source themselves**,
748
+ each covering a different subset of the five ways a file can name another; **10 hand-wrote the same
749
+ re-export visitor**. None of that was decided — it was the order the rules were written in, and a
750
+ single fix to how a path is read had to be found rule by rule.
751
+
752
+ **`src/rules/lib/` is now the one door.** `paths.ts` (`normalise`, `fileOf`), `imports.ts`
753
+ (`sourceOf` over `import` · `export … from` · `export * from` · `import()` · `require()`,
754
+ `isTypeOnly`, `resolveRelative`), `visitors.ts` (`onImportSource` — one visitor set over all five
755
+ shapes — and `mergeVisitors`), `tiers.ts` (`tierOf`, `featureOf` beside `inTier`).
756
+
757
+ **The ten direction rules are declarations over one `directionRule()` factory**: a scope, a
758
+ forbidden predicate, the messages, the fixShape. Each file is 17–57 lines and holds its patterns and
759
+ its wording, nothing mechanical. Rule ids, message ids, message text, options and schemas are
760
+ unchanged, and parity over the kit's corpus and over a consumer tree reported lost 0 / gained 0 /
761
+ changed 0 at every step.
762
+
763
+ **Behaviour change — every direction rule now judges an import by where it LANDS.**
764
+ `cell-must-not-compose-cell` and `layer-walls` resolved a relative source against the importing file;
765
+ the other eight read the string, so `../../billing/organelles/totals` — which names no feature and no
766
+ tier until it is resolved — was invisible to them, and the same breach was a finding or not
767
+ depending on which rule looked. One resolver now, for all of them. In practice this reaches
768
+ `organelle-dependency` and `no-cross-feature-stores`: a relative climb into another feature's
769
+ organelle or store is a finding where it used to be silent. Measured on a consumer's tree
770
+ (`apps` + `packages`, its own config): 135 findings before, 135 after — its relative cross-feature
771
+ climbs are into `shared/`, which the `sharedFeatures` option permits.
772
+
773
+ `RULES.md` carries the named decision, the parity artefact path, and a Core column naming the
774
+ factory or helper behind each of the 56 rules.
775
+
776
+ ## 0.3.0
777
+
778
+ ### Minor Changes
779
+
780
+ - Three things a consumer asked for.
781
+
782
+ **`./discover-atoms` reads the element an atom wraps from its props type.** It used to take the
783
+ first lowercase JSX-looking token in the file, which on a real shadcn tree returns `string`,
784
+ `number` and `typeof` (a TypeScript generic reads as JSX to a regex) and `path` / `g` (SVG
785
+ internals), while missing `button` entirely — a Radix atom writes
786
+ `const Comp = asChild ? Slot.Root : 'button'` and never a `<button>` tag. Now:
787
+ `ComponentProps<'button'>`, `ComponentPropsWithoutRef` / `WithRef`,
788
+ `HTMLAttributes<HTMLSelectElement>` (the DOM interface mapped back to its tag) and
789
+ `React.ButtonHTMLAttributes`, then the Radix slot form, then a JSX scan filtered through an
790
+ allow-list of HTML tags. A `.stories.tsx` is no longer read as an atom. Exports and signatures
791
+ unchanged.
792
+
793
+ **`no-raw-html-atoms` is silent inside the atom tier**, where a raw element is the point of the
794
+ file, and takes a `scope` option to narrow it to named tiers. The default is every file EXCEPT the
795
+ atom tier — not an allow-list of the five upper tiers, which would quietly stop reporting every
796
+ file in no tier folder at all (an admin widget, a route segment). Its diagnostic now says "above
797
+ the atom tier" where it said "in molecules/compounds/cells/tissues".
798
+
799
+ **New rule `emit-declares-attempts`** (preset `backend-medusa`): Medusa's `emitEventStep` defaults
800
+ `attempts` to 1 and the worker treats only `attempts > 1` as a configured retry, so an emit with
801
+ `{ priority }` and no `attempts` is droppable while reading as deliberate. A non-literal `attempts`
802
+ passes — the rule cannot evaluate a shared constant and does not pretend to. `emitters` is an
803
+ option, default `["emitEventStep"]`.
804
+
805
+ ## 0.2.1
806
+
807
+ Version only — the three `@geonosis/*` packages move as one fixed group, and nothing in this
808
+ package changed in 0.2.1. The release is `@geonosis/ratchet`'s: `--prove`, `--exclusive`, and
809
+ `typecheckErrors` refusing on an unbuilt workspace.
810
+
811
+ ## 0.2.0
812
+
813
+ ### Minor Changes
814
+
815
+ - Loose coupling, first slice — and a posture change that will break a config on purpose.
816
+
817
+ **`@geonosis/lint-parity` is a new package.** Findings parity, rule reach, the corpus-manifest
818
+ contract and the `geonosis-lint-parity` bin now live on their own. The tool knows no rule name and
819
+ no plugin: two oxlint configs and a tree are the whole input, and a corpus arrives as a directory
820
+ whose own `manifest.json` says which rules it speaks for. A parity tool welded to one rule plugin
821
+ can only ever check that plugin.
822
+
823
+ **BREAKING (plugin): the `geonosis-lint-parity` bin and the `./parity` export are gone.** Install
824
+ `@geonosis/lint-parity` and pass `--corpus node_modules/@geonosis/oxlint-plugin-biological-architecture/corpus`
825
+ — the plugin still ships the corpus, which is the part that is genuinely its own. `--corpus` now
826
+ requires its directory.
827
+
828
+ **BREAKING (plugin): a rule enabled without the options it needs refuses the run.** `no-brand-names`,
829
+ `no-raw-html-atoms`, `documents-share-one-table`, `document-sagas-are-generic`,
830
+ `step-opens-its-own-cell` and `layer-walls` throw from `create()` naming the missing option, instead
831
+ of returning an empty visitor. A consumer that enabled one bare will fail lint until it configures
832
+ the rule or turns it off — which is the point: one consumer ran `no-raw-html-atoms` at "error" for
833
+ months with no element→atom map, over 136 raw elements, and the gate was green. An enabled rule that
834
+ cannot fire is indistinguishable from compliance. A rule nobody enabled still does nothing.
835
+
836
+ **New rule `layer-walls`, new preset `tooling`.** A repo declares its own layers as data —
837
+ `{ name, paths, mayImport }` — and the rule enforces the direction between them: relative sources
838
+ resolved against the importing file first, `import()`, `require()` and both re-export forms visited,
839
+ externals not walls unless asked. The kit enables it on itself.
840
+
841
+ The plugin also ships type declarations now, for all three entries.
842
+
843
+ ## 0.1.2
844
+
845
+ ### Patch Changes
846
+
847
+ - Four findings from the 0.1.1 code review, plus a version that disagreed with itself.
848
+
849
+ **`--corpus` scopes its verdict to the rules its corpus can speak for.** It built the list of
850
+ configured rules from every rule id in both configs, but the shipped corpus only exercises this
851
+ plugin's. Any real consumer config also enables `eqeqeq`, `no-throw-literal` and
852
+ `typescript/no-explicit-any`, so `--corpus` reported `FAIL — fired under neither` on every
853
+ adoption. A corpus now declares its own scope in a `manifest.json` generated from the plugin's
854
+ rules export; rules it does not name are listed under "outside this corpus" with no verdict, and
855
+ neither the exit code nor `neither` considers them. `--corpus <dir>` points the run at another
856
+ plugin's corpus. Matching is on full `plugin/rule` ids on both sides, so
857
+ `typescript(no-unused-vars)` can never stand as evidence for somebody else's `no-unused-vars`.
858
+
859
+ **Parity identifies a finding by its place and its rule, not its wording.** `path:line:col` plus
860
+ `plugin(rule)`; message and severity are excluded. Diffing whole lines meant a rule whose message
861
+ was copy-edited read as one finding lost and one gained — exit 1, a stopped adoption over a
862
+ sentence. Those pair up in a new `messageChanged` list, reported under its own heading and written
863
+ to `changed.txt`, and never reach the exit code. Multiset semantics are unchanged.
864
+
865
+ **`oxlintRule` gets the cross-check the error and warning counters already had.** It counted
866
+ `(rule)` anywhere in the output, so a format that printed rule ids differently read as 0 and the
867
+ ratchet banked the "win" by rewriting the baseline down — and a quoted source line containing
868
+ `(a-rule-name)` counted as a finding. The count now comes off recognised finding lines only, and a
869
+ run with findings where none attributes itself readably is refused. The strict-config copy also
870
+ upgrades the `["off", { … }]` array form, which is how every rule with options is parked
871
+ mid-migration.
872
+
873
+ **The warnings-only window is documented and closable.** oxlint exits 0 when a run found only
874
+ warnings, so the counters' non-zero-exit backstop cannot catch an unrecognised format on such a
875
+ run. The README states this plainly and says why `--deny-warnings` is not the fix. New optional
876
+ counter param `expectFormat: "unix" | "agent" | "default"` refuses output in no known shape even on
877
+ a clean exit, once npm's own chatter is stripped. Unset is the default and changes nothing.
878
+
879
+ **The plugin reported `meta.version` 0.1.0 while shipping 0.1.1.** It is read from `package.json`
880
+ now, and a test asserts the two agree.
881
+
882
+ ## 0.1.1
883
+
884
+ ### Patch Changes
885
+
886
+ - The defects the first real adoption exposed.
887
+
888
+ - **The oxlint counters had never measured anything.** `oxlintErrors` and `oxlintWarnings` matched
889
+ `/: error /`, which is the shape of oxlint's compact `agent` format alone, while every config asks
890
+ for `--format=unix`. They now read all three shapes oxlint prints and cross-check the count
891
+ against the tool's own summary, raising `CounterError` rather than returning a number they cannot
892
+ corroborate — a blind counter reports 0 and lets the ratchet rewrite the baseline DOWN as a win.
893
+ - **`cell-must-not-compose-cell` fired on the barrel `cells-folder-index-is-barrel` demands.**
894
+ dielime's adoption reported 32 errors, every one a `cells/index.ts` re-exporting siblings, which
895
+ is the shape the other rule requires at the same severity. The two rules now read one shared list
896
+ of barrel filenames. A real cell importing or re-exporting another cell still fires.
897
+ - **`geonosis-lint-parity`.** The adoption proof — same tree, two configs, a findings diff — as a
898
+ bin instead of forty tool calls. Exits 1 when a finding the old config caught is missing under the
899
+ new one. `--corpus` answers the prior question first: does each configured rule fire anywhere at
900
+ all? The package now ships the reach corpus that makes that answerable, with a file for every one
901
+ of its 54 rules.
902
+ - **The ratchet shows what grew.** A regressed counter prints the last ten lines of its own command
903
+ output under the `<-- REGRESSED` line, so the report answers the question it used to raise.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geonosis/oxlint-plugin-biological-architecture",
3
- "version": "2.3.1",
3
+ "version": "2.4.2",
4
4
  "types": "./dist/index.d.ts",
5
5
  "description": "Biological tier architecture as lint — the union of the dielime and during.day rule sets, shipped as presets.",
6
6
  "keywords": [
@@ -36,6 +36,7 @@
36
36
  }
37
37
  },
38
38
  "files": [
39
+ "CHANGELOG.md",
39
40
  "RULES.md",
40
41
  "corpus",
41
42
  "dist"
@@ -44,7 +45,7 @@
44
45
  "oxlint": ">=1.77"
45
46
  },
46
47
  "devDependencies": {
47
- "@geonosis/lint-parity": "2.3.1"
48
+ "@geonosis/lint-parity": "2.4.2"
48
49
  },
49
50
  "engines": {
50
51
  "node": ">=22"