@geonosis/oxlint-plugin-biological-architecture 2.3.1 → 2.4.3

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