@geonosis/oxlint-plugin-biological-architecture 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/RULES.md +939 -0
- package/corpus/engine/bad-nested-zero-retry.ts +9 -0
- package/dist/index.js +24 -9
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -201,8 +201,9 @@ Then find the stray copy, and mind that the answer is package-manager-specific:
|
|
|
201
201
|
|
|
202
202
|
## Where to read on
|
|
203
203
|
|
|
204
|
-
- `RULES.md` — every rule, its preset, which source tree it came from,
|
|
205
|
-
resolved
|
|
204
|
+
- `RULES.md` — in this tarball: every rule, its preset, which source tree it came from, how each
|
|
205
|
+
divergence was resolved, the option each gated rule refuses without, and a generated line per rule
|
|
206
|
+
naming every option it accepts.
|
|
206
207
|
- `skills/geonosis-code/SKILL.md` in the repository — **generated** from every rule's `fixShape`:
|
|
207
208
|
the shape each gate accepts, in one short document meant to be read before writing code.
|
|
208
209
|
|
package/RULES.md
ADDED
|
@@ -0,0 +1,939 @@
|
|
|
1
|
+
# Rule inventory
|
|
2
|
+
|
|
3
|
+
Read from the two source trees on 2026-08-29:
|
|
4
|
+
|
|
5
|
+
- `dielime/packages/eslint-plugin-biological-architecture` — **36 rules**
|
|
6
|
+
- `during.day/packages/eslint-plugin-biological-architecture` — **52 rules** (51 when the union was
|
|
7
|
+
cut; `step-opens-its-own-cell` landed there hours later and was folded in afterwards)
|
|
8
|
+
|
|
9
|
+
One plugin name, two rule sets, no shared code. **33 rules had the same filename in both trees; only
|
|
10
|
+
3 of those 33 were byte-identical.** A fix in one never reached the other.
|
|
11
|
+
|
|
12
|
+
The union here is **54 rules**, plus twenty-two the kit wrote for itself — `layer-walls`,
|
|
13
|
+
`emit-declares-attempts`, `cell-no-cross-feature-organelles`, the `backend-medusa` ten of 1.0.0,
|
|
14
|
+
`no-unregistered-plugin-dir` + `router-schema-parity`, the `themekit` two,
|
|
15
|
+
`no-mock-db-in-integration`, `todo-names-its-plan`, `boundary-time-is-primitive`,
|
|
16
|
+
`durable-body-reads-through-steps` and `subpath-over-barrel` — **76**.
|
|
17
|
+
|
|
18
|
+
| | count |
|
|
19
|
+
|---|---|
|
|
20
|
+
| in both trees, byte-identical | 3 |
|
|
21
|
+
| in both trees, **diverged** | 30 |
|
|
22
|
+
| dielime only | 3 |
|
|
23
|
+
| during.day only | 18 |
|
|
24
|
+
| **union** | **54** |
|
|
25
|
+
| written here (`layer-walls`, `emit-declares-attempts`, `cell-no-cross-feature-organelles`) | 3 |
|
|
26
|
+
| written here, `backend-medusa` (1.0.0) | 7 |
|
|
27
|
+
| written here, the integration seam (1.0.0) | 2 |
|
|
28
|
+
| **total** | **66** |
|
|
29
|
+
| written here, `backend-medusa` (1.0.0) | 10 |
|
|
30
|
+
| **total** | **67** |
|
|
31
|
+
| written here, `themekit` (1.0.0) | 2 |
|
|
32
|
+
|
|
33
|
+
## How divergence was resolved
|
|
34
|
+
|
|
35
|
+
Two shapes of divergence, and one resolution rule each.
|
|
36
|
+
|
|
37
|
+
**dielime broadened the path patterns.** Every dielime rule scoped its file test as
|
|
38
|
+
`(?:features/[^/]+/)?<tier>/`, so it fires on a FLAT kit (`packages/ui/src/tissues/…`) as well as a
|
|
39
|
+
feature-sliced app. during.day's kept `features/<name>/<tier>/`. **The broader pattern wins** — a
|
|
40
|
+
rule that reaches more files is the stricter rule.
|
|
41
|
+
|
|
42
|
+
**during.day broadened the node coverage.** Its August 2026 sweep added
|
|
43
|
+
`ExportAllDeclaration` / `ExportNamedDeclaration` handlers to every import-direction rule (a
|
|
44
|
+
re-export names a module exactly as an import does), and turned `tissue-no-hooks` from an import
|
|
45
|
+
check into a hook-CALL check — the hole three review rounds missed was a tissue reaching a store
|
|
46
|
+
through a `lib/` hook. **The wider visitor set wins**, for the same reason.
|
|
47
|
+
|
|
48
|
+
Where a divergence was a false-positive fix rather than a reach change (own-sibling stem tolerance,
|
|
49
|
+
`.parts.tsx` entrypoint scoping, re-export shims, react-email and Radix as composition sources,
|
|
50
|
+
`Kind`-vocabulary props), it was kept: both repos' fixtures depend on those, and a rule that fires on
|
|
51
|
+
an accepted convention is a rule people learn to ignore.
|
|
52
|
+
|
|
53
|
+
**Both repos' test suites are ported and green** — during.day's 300-odd hand-built-AST tests, its
|
|
54
|
+
`real-source-tests` cases through the real binary, and dielime's 45-case oxlint fixture lock.
|
|
55
|
+
|
|
56
|
+
## Named decisions
|
|
57
|
+
|
|
58
|
+
### `cell-must-be-stateful` (dielime) vs `cell-handles-are-scoped` (during.day) — **both kept, one renamed**
|
|
59
|
+
|
|
60
|
+
These share only the word "cell", and they mean different things by it.
|
|
61
|
+
|
|
62
|
+
- dielime's `cell-must-be-stateful` is the **biological tier**: a file in `cells/` whose whole family
|
|
63
|
+
is props-in / JSX-out is a compound in the wrong folder. It is the inverse of
|
|
64
|
+
`compound-must-be-stateless`. → kept as-is, `frontend`.
|
|
65
|
+
- during.day's `cell-handles-are-scoped` is a **Cloudflare D1 database cell**: a worker that opens a
|
|
66
|
+
cell handle must wrap every invocation in `withCells` or it leaks a pool per call. Nothing to do
|
|
67
|
+
with a tier. → kept, **renamed `worker-handles-are-scoped`**, `backend-d1`.
|
|
68
|
+
|
|
69
|
+
In this kit "cell" is a tier. A backend rule that means something else by it would teach the wrong
|
|
70
|
+
vocabulary at every diagnostic, so the backend one gave up the word.
|
|
71
|
+
|
|
72
|
+
### The time door is an option, and its exemption is a LIST OF MODULES (1.5.0)
|
|
73
|
+
|
|
74
|
+
`time-through-the-door` shipped with `/packages/utils/src/time.ts` and four exemption directories
|
|
75
|
+
compiled in — a rule exactly one repo could enable, and the thing pinning that repo's last
|
|
76
|
+
generic-utility package in place, because moving the door would have silenced the rule guarding it.
|
|
77
|
+
The door is now the `door` option and the directories are `exempt`, both regex sources over the
|
|
78
|
+
normalised filename, and the rule refuses the run when `door` is missing. The LIBRARY it polices was
|
|
79
|
+
compiled in for one release longer — `dayjs`, so a repo on date-fns kept the raw-`Date` half and
|
|
80
|
+
lost the half that catches a second configured formatter, which is the defect that half exists for.
|
|
81
|
+
It is the `libraries` list now, defaulting to `['dayjs']`, and every diagnostic names the library
|
|
82
|
+
the repo declared.
|
|
83
|
+
|
|
84
|
+
`boundary-time-is-primitive` takes the same fact from the other side and answers it differently on
|
|
85
|
+
purpose. Its exemption is `doors`: modules NAMED IN FULL, one per entry, never a pattern. A rule
|
|
86
|
+
scoped by a glob grants its exemption to wherever the glob stops, and nothing then records who
|
|
87
|
+
decided the boundary or counts what accumulated on the far side — a consumer's path-exempted module
|
|
88
|
+
took on seven unchartered exports, one of them a write, lint-free, in eleven commits. A list of
|
|
89
|
+
module names is a number a reader can hold: a repo has one door, or it has three and knows it.
|
|
90
|
+
|
|
91
|
+
### An enabled rule that cannot fire **refuses the run** (0.2.0)
|
|
92
|
+
|
|
93
|
+
This replaces the "does nothing until configured" posture below, and it is the one breaking change
|
|
94
|
+
in 0.2.0 that a consumer will feel.
|
|
95
|
+
|
|
96
|
+
Making every repo-specific list an option (law 6) has an obvious implementation and a wrong one:
|
|
97
|
+
return an empty visitor when the option is missing. The result is a rule that is enabled, graded
|
|
98
|
+
`"error"`, and **indistinguishable from compliance**. dielime ran `no-raw-html-atoms` at `"error"`
|
|
99
|
+
for months with no element→atom map. 136 raw elements in the tree. Zero findings. A green gate. It
|
|
100
|
+
was found by hand, not by a gate, because there was no gate — only a rule with nothing to compare
|
|
101
|
+
against, which is the silent-fallback class one level up from a counter reading zero under a format
|
|
102
|
+
it cannot parse.
|
|
103
|
+
|
|
104
|
+
So the posture splits in two:
|
|
105
|
+
|
|
106
|
+
- a rule **nobody enabled** does nothing — that is oxlint's job, and it needs no help from us;
|
|
107
|
+
- a rule that **is** enabled without the options it needs is a **config error**, and `create()`
|
|
108
|
+
throws, naming the rule and the missing option. oxlint refuses and exits non-zero.
|
|
109
|
+
|
|
110
|
+
**Nineteen** rules take this, and the list is not kept by hand: `src/rules/option-gated.ts` names
|
|
111
|
+
each with the option its refusal must say and a bag that makes it run clean, and
|
|
112
|
+
`src/option-gated.test.ts` asks every rule the plugin exports whether it refuses a bare `create()`
|
|
113
|
+
and fails when one of them is not on the list — or is on it and no longer refuses. It found seven
|
|
114
|
+
rules refusing with nobody proving it through the binary, `time-through-the-door` and
|
|
115
|
+
`boundary-time-is-primitive` among them. An empty list counts as missing: it is the same nothing
|
|
116
|
+
spelled longer. One helper, `src/rules/lib/options.ts`, so the
|
|
117
|
+
wording cannot drift rule to rule:
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
biological-architecture/<rule> is enabled but its `<option>` option is nothing, so it can never
|
|
121
|
+
fire. Configure `<option>`, or turn the rule off — an enabled rule that cannot fire reads exactly
|
|
122
|
+
like a rule that found nothing.
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
One wrinkle worth knowing before it surprises someone: oxlint renders a plugin error as a diagnostic
|
|
126
|
+
with **no source span**, and `--format=unix` has nowhere to put one, so it prints `:0:0:` and no
|
|
127
|
+
wording. The message is intact in the default and `json` formats. Under unix the run is still
|
|
128
|
+
non-zero, and the summary counts a problem no line accounts for — which is exactly the shape
|
|
129
|
+
`@geonosis/lint-parity` refuses to read as zero findings. Unreadable, never invisible.
|
|
130
|
+
|
|
131
|
+
**The kit takes its own medicine**: its `.oxlintrc.json` enabled `no-brand-names` and
|
|
132
|
+
`no-raw-html-atoms` bare, and both were inert. `no-brand-names` now names the two brands that must
|
|
133
|
+
never re-enter kit source (the ones the fork had compiled into its regex) — proved reaching by
|
|
134
|
+
adding one to a source file and watching it fire.
|
|
135
|
+
|
|
136
|
+
### `no-brand-names` (dielime only) — brand list is now an **option**
|
|
137
|
+
|
|
138
|
+
The rule shipped with a two-brand regex and a matching CDN-host allowance compiled into it. It could
|
|
139
|
+
only ever serve the repo that wrote it. Now:
|
|
140
|
+
|
|
141
|
+
```jsonc
|
|
142
|
+
"biological-architecture/no-brand-names": [
|
|
143
|
+
"error",
|
|
144
|
+
{ "brands": ["acme"], "allowedSubstrings": ["media.acme.com"] }
|
|
145
|
+
]
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Enabled with no `brands`, it refuses the run — see the decision above.
|
|
149
|
+
|
|
150
|
+
### `step-opens-its-own-cell` — one law, two spellings, every fact an **option**
|
|
151
|
+
|
|
152
|
+
The law is one sentence: a durable instance hibernates between steps and replays them on retry, so
|
|
153
|
+
each unit of work opens the handle it uses rather than sharing one opened once per run. WHERE that
|
|
154
|
+
is declared is what has moved, because the org now runs two engines at once.
|
|
155
|
+
|
|
156
|
+
`@geonosis/workflows` has a seam for it — `scope.perStep` on the runtime a durable host builds — so
|
|
157
|
+
the finding is a host that declares none. `hosts` names the calls that build one (default
|
|
158
|
+
`createRuntime`, `entrypointFor`, the engine's own two) and `scopeKey` the key (default `perStep`);
|
|
159
|
+
`within` is the tree, and is required:
|
|
160
|
+
|
|
161
|
+
```jsonc
|
|
162
|
+
"biological-architecture/step-opens-its-own-cell": [
|
|
163
|
+
"error",
|
|
164
|
+
{ "within": "packages/workflows/src/" }
|
|
165
|
+
]
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The scope is read anywhere inside the host call rather than at `scope.perStep` exactly, because the
|
|
169
|
+
Cloudflare adapter's entry declares it one closure down —
|
|
170
|
+
`entrypointFor({ runtime: (env) => createRuntime({ scope: … }) })` — and a check anchored on the
|
|
171
|
+
outer object would report every correct one of them. A host whose config cannot be read WHOLE (a
|
|
172
|
+
spread, a variable) is not judged at all: unreadable is not the same as absent, and only one of the
|
|
173
|
+
two is a finding.
|
|
174
|
+
|
|
175
|
+
`sagaflow-js` has no such seam. A repo still on it wrapped `step` in a file of its own, and the
|
|
176
|
+
finding there is a saga importing the engine's `step` around that wrapper — reachable by naming
|
|
177
|
+
`engine` and `wrapper`, and off entirely for a repo that names no engine:
|
|
178
|
+
|
|
179
|
+
```jsonc
|
|
180
|
+
"biological-architecture/step-opens-its-own-cell": [
|
|
181
|
+
"error",
|
|
182
|
+
{
|
|
183
|
+
"engine": "sagaflow-js",
|
|
184
|
+
"within": "packages/workflows/src/",
|
|
185
|
+
"wrapper": "packages/workflows/src/step\\.ts$"
|
|
186
|
+
}
|
|
187
|
+
]
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Enabled with no `within`, it refuses the run. Type-only imports never fire: they bring no handle, so
|
|
191
|
+
they can share none. The diagnostic quotes the option values rather than any repo's paths, and the
|
|
192
|
+
preset is its own — the invariant is true of a DURABLE run that hibernates between steps and false
|
|
193
|
+
of an inline saga sharing the request's connection, so `backend-workflows` would have been a repo
|
|
194
|
+
fact wearing a preset's name.
|
|
195
|
+
|
|
196
|
+
### `layer-walls` (written here) — the `no-restricted-imports` factory, as data
|
|
197
|
+
|
|
198
|
+
Neither source repo had it; a third repo of the user's, musa-loading-plan-engine, hand-rolled it as
|
|
199
|
+
an ESLint `no-restricted-imports` factory in `eslint.config.mjs` (kernel ← auto/manual ← tools;
|
|
200
|
+
manual never imports auto). Every layered repo writes that file, and every one of them writes it in
|
|
201
|
+
a different shape, so it is the kit's job.
|
|
202
|
+
|
|
203
|
+
The layers are **data**: `{ name, paths, mayImport }`, `paths` being regex sources tested against the
|
|
204
|
+
normalised filename. A layer may always import itself; `mayImport` lists the others. A file in no
|
|
205
|
+
declared layer is unconstrained — walls are declared, never inferred — and a target in no layer is
|
|
206
|
+
not a wall, so a repo can wall one corner of its tree and leave the rest alone.
|
|
207
|
+
|
|
208
|
+
```jsonc
|
|
209
|
+
"biological-architecture/layer-walls": ["error", {
|
|
210
|
+
"layers": [
|
|
211
|
+
{ "name": "core", "paths": ["packages/x/src/core/"], "mayImport": [] },
|
|
212
|
+
{ "name": "cli", "paths": ["packages/x/src/cli\\.ts$"], "mayImport": ["core"] }
|
|
213
|
+
]
|
|
214
|
+
}]
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Three decisions inside it:
|
|
218
|
+
|
|
219
|
+
- **A relative source is resolved against the importing file before it is classified.** `../../shell/boot`
|
|
220
|
+
carries no layer name at all, and a rule matching the string alone lets every wall be walked
|
|
221
|
+
around with two dots. Only `cell-must-not-compose-cell` in this plugin does this today; plan 018
|
|
222
|
+
K1.2 gives every direction rule the same resolver.
|
|
223
|
+
- **A package specifier and a `node:` builtin are not walls by default.** A layer that may import no
|
|
224
|
+
other layer still runs on a machine with a filesystem. `externals: "deny"` is the opt-in for a
|
|
225
|
+
layer that really must reach nothing outside the declared set. A bare specifier a layer's `paths`
|
|
226
|
+
claims is still classified by it, which is what lets `parity` be forbidden from importing
|
|
227
|
+
`@geonosis/oxlint-plugin-biological-architecture` by name.
|
|
228
|
+
- **`ImportExpression` and `require()` are visited too**, beside the two re-export forms. A wall
|
|
229
|
+
visited only on `ImportDeclaration` is a wall with three doors left open.
|
|
230
|
+
|
|
231
|
+
Its own preset, `tooling`: it enforces nothing until a repo declares its layers, and a repo can want
|
|
232
|
+
layer discipline in its tooling without wanting a React tier vocabulary anywhere near it. **The kit
|
|
233
|
+
enables it on itself** in the root `.oxlintrc.json` — seven layers across the three packages, the
|
|
234
|
+
load-bearing one being `parity`, which may import nothing, so `@geonosis/lint-parity` cannot grow an
|
|
235
|
+
import of the plugin it exists to check.
|
|
236
|
+
|
|
237
|
+
### `emit-declares-attempts` (written here, from a consumer's landmine) — 0.3.0
|
|
238
|
+
|
|
239
|
+
Medusa's `emitEventStep` defaults `attempts` to 1, and the redis worker treats only `attempts > 1`
|
|
240
|
+
as "a retry was configured". An emit that passes `{ priority }` and **no** `attempts` is therefore
|
|
241
|
+
droppable on its first failure while reading as deliberate — it has an options bag, it names a
|
|
242
|
+
priority, a reviewer moves on. The options bag is what hides it.
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
emitEventStep({ data, eventName: 'order.placed', options: { attempts: 3, priority } })
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
What it judges, and what it refuses to guess:
|
|
249
|
+
|
|
250
|
+
- no `options` at all, or an `options` with no `attempts` → reported;
|
|
251
|
+
- a literal `attempts` of 1 or less → reported, because that is the default spelled out;
|
|
252
|
+
- a **non-literal** `attempts` — `RETRY_BACKGROUND.attempts`, the shared constant — **passes**. The
|
|
253
|
+
rule cannot evaluate it, and one that guessed here would push every repo into inlining the number
|
|
254
|
+
at each call site, which is the opposite of what the shared constant is for;
|
|
255
|
+
- a payload or an options bag it cannot read whole (a variable, a spread) → passes, same reason.
|
|
256
|
+
|
|
257
|
+
The emitter names are the option `emitters`, default `["emitEventStep"]`: a repo that wraps the emit
|
|
258
|
+
under its own name says so instead of forking the rule. Emptied out (`emitters: []`) it refuses the
|
|
259
|
+
run, like every other option a rule cannot fire without.
|
|
260
|
+
|
|
261
|
+
Preset `backend-medusa`, which is twelve rules now.
|
|
262
|
+
|
|
263
|
+
**One correction to the brief that asked for it**: dielime does not guard 50 emit sites this way
|
|
264
|
+
today. It has **four** `emitEventStep` calls (`create-sample-order`, `ingest-shipment-milestone`,
|
|
265
|
+
`convert-sample-to-order` ×2), and **all four would fire** — three carry
|
|
266
|
+
`options: { priority: EventPriority.CRITICAL }` with no attempts, one carries no options at all.
|
|
267
|
+
That is the fixture, and it is worth more than the guarded version would have been.
|
|
268
|
+
|
|
269
|
+
### The integration seam (1.0.0) — two rules, two new presets, both option-gated
|
|
270
|
+
|
|
271
|
+
`no-unregistered-plugin-dir` (preset **`integrations`**) and `router-schema-parity` (preset
|
|
272
|
+
**`backend-trpc`**). Each gets its own preset because each enforces nothing until a repo says where
|
|
273
|
+
its directories are, and because having integrations says nothing about having a tRPC mirror.
|
|
274
|
+
|
|
275
|
+
**`no-unregistered-plugin-dir`.** Midday ships 35 `app-store` directories and **eight are reachable
|
|
276
|
+
from nothing** — `cal`, `deel`, `dropbox`, `google-drive`, `polar`, `raycast`, `stripe`, `zapier`.
|
|
277
|
+
They break nothing, which is why they survived: they read as integrations to anyone counting
|
|
278
|
+
directories. The rule fires on the manifest file of a directory the registry does not name, and the
|
|
279
|
+
registry is read as SOURCE — a rule that had to import it would need the repo's whole resolution set
|
|
280
|
+
up inside a linter, and every string in it is a name the registry knows whether it is spelled as an
|
|
281
|
+
id or as an import path.
|
|
282
|
+
|
|
283
|
+
dielime is the fixture that matters just as much: measured, **zero** of its provider directories are
|
|
284
|
+
unregistered. A rule that fired on the clean tree would be off within a day. And its third seam has
|
|
285
|
+
no directories at all — `email-gmail.ts` and `whatsapp-360dialog.ts` sit flat beside their registry —
|
|
286
|
+
so it is out of scope because nothing matches `<root>/<dir>/<manifest>`, not by a special case.
|
|
287
|
+
|
|
288
|
+
*`registry` takes a list, because a registry can be split.* dielime's `medusa-config.ts` imports
|
|
289
|
+
`medusa-config.fulfillment.ts`, and the carrier directories are registered in the half that is
|
|
290
|
+
imported. Reading only the file the option named made two REGISTERED directories a standing FAIL
|
|
291
|
+
there — a false positive on a working tree, which is how a rule gets turned off. The halves are
|
|
292
|
+
listed, not followed: chasing imports out of a registry would need the repo's whole module
|
|
293
|
+
resolution set up inside a linter, and would guess. One unreadable half is the same silence a
|
|
294
|
+
single unreadable file has always bought — a configuration answer, not a partial verdict taken from
|
|
295
|
+
the halves that happened to open. The refusal now names the accepted shape too (#75), because a
|
|
296
|
+
repo that reached for a list was told `should be string` and could not learn from the rejection
|
|
297
|
+
whether a list was a shape the rule had or a shape it lacked.
|
|
298
|
+
|
|
299
|
+
*The defect the two test layers caught between them.* The rule's first version scanned every quoted
|
|
300
|
+
string in the registry, and the corpus registry's own comment said "the third is `dead-cal`" — in
|
|
301
|
+
backticks. The scanner counted it as a registration and the rule went silent on the one directory
|
|
302
|
+
its fixture exists to catch. The hand-built nodes could not find this: a hand-built registry has no
|
|
303
|
+
prose in it. Comments are stripped first now, and the direction is the point — a name that appears
|
|
304
|
+
only in a comment is usually the note explaining why the directory was abandoned.
|
|
305
|
+
|
|
306
|
+
**`router-schema-parity`.** during.day holds the mirror by hand at 21 of 22 routers and 21 of 23
|
|
307
|
+
contracts; Midday holds its own at 33 of 37. A router with no schema is a wire contract written
|
|
308
|
+
wherever the handler happens to say it; a schema with no router is a contract nothing serves.
|
|
309
|
+
|
|
310
|
+
Two facts were measured before the rule was written, and both changed its shape:
|
|
311
|
+
|
|
312
|
+
1. during.day's two halves are in **different packages** — `apps/api/src/trpc/routers` against
|
|
313
|
+
`packages/api-contract/src` — so the options are repo-root-relative. A package-relative option
|
|
314
|
+
could not name them both.
|
|
315
|
+
2. during.day's three apparent gaps are ONE deliberate arrangement: `routers/documents-mounted.ts`
|
|
316
|
+
mounts the invoice and purchase-order entities and imports both contracts. A strict filename
|
|
317
|
+
mirror reports all three and is wrong all three times, on the very tree the rule was built from.
|
|
318
|
+
So `mounts` (`{ "documents-mounted": ["invoice", "purchase-order"] }`) is config, and the corpus
|
|
319
|
+
carries the case both ways: the mount declared and the three findings it suppresses.
|
|
320
|
+
|
|
321
|
+
### Two options the consumers forced (1.0.0) — both of them silent zeros
|
|
322
|
+
|
|
323
|
+
Neither of these came from review. Both came from running the rule read-only over the consumer and
|
|
324
|
+
then running a CONTROL that should have made the same files fire.
|
|
325
|
+
|
|
326
|
+
**`no-unregistered-plugin-dir.manifests` is required, with no default.** The rule first shipped a
|
|
327
|
+
default entry-file list — `index.ts`, `index.tsx`, `config.ts`. Over dielime's two provider roots it
|
|
328
|
+
reported zero findings, and every directory there IS registered, so the run read as a pass. It was
|
|
329
|
+
not one: dielime's plugin directories contain `service.ts` and no index at all, so the rule matched
|
|
330
|
+
no file and answered a question it had never asked. The control — the same files, with a registry
|
|
331
|
+
naming none of them — also reported zero, which is what exposed it. With `manifests: ["service.ts"]`
|
|
332
|
+
the control fires on all three directories and the real config on none.
|
|
333
|
+
|
|
334
|
+
**`emit-declares-attempts.budgetHelpers`, empty by default.** The claim this rule carried was that it
|
|
335
|
+
covers dielime's emit sites, so that repo's 132-line source walker could be deleted. Measured: all
|
|
336
|
+
**54** `emitEventStep` sites spell the bag `options: retryableEvent(…)` — zero object literals, zero
|
|
337
|
+
missing bags — and a call is precisely the shape the rule declines to judge. It reported zero, and
|
|
338
|
+
that zero proved only that no bag was absent. Named as a budget helper, a call to `retryableEvent`
|
|
339
|
+
passes and a call to anything else is reported as a budget nobody can check; the control naming a
|
|
340
|
+
helper dielime does not use reports all 54. Empty by default, so no existing consumer moves.
|
|
341
|
+
|
|
342
|
+
Both are the same failure as a counter that reads zero under a format it cannot parse, and both were
|
|
343
|
+
invisible to the rules' own fixtures — a fixture is written to match the rule, which is exactly what
|
|
344
|
+
a consumer's tree does not do.
|
|
345
|
+
|
|
346
|
+
### `no-mock-db-in-integration` (1.0.0, preset **`testbed`**) — the rule both consumers pass
|
|
347
|
+
|
|
348
|
+
The only rule in the plugin whose landmine is in **neither** source repo. Measured on 2026-08-30:
|
|
349
|
+
|
|
350
|
+
| tree | `mockDb` | `vi.mock('…db…')` | `vi.mock(` in integration suites |
|
|
351
|
+
|---|---:|---:|---:|
|
|
352
|
+
| dielime | 0 | 0 | **0** of 106 integration files |
|
|
353
|
+
| during.day | 0 | 0 | 0 (`vi.mock` appears nowhere in the tree) |
|
|
354
|
+
| Midday | 32 references in 4 files | — | the gate |
|
|
355
|
+
|
|
356
|
+
Midday's shape: `apps/api/src/__tests__/setup.ts:80` exports `mockDb = createMockDb()`;
|
|
357
|
+
`helpers/test-context.ts:27` sets `db: mockDb`; **35 of the 43** test files under `__tests__/` import
|
|
358
|
+
that helper, and all 35 sit in `__tests__/trpc/`. They assert that a procedure calls what it says it
|
|
359
|
+
calls, and nothing about what the database returns. The repo's own `packages/db/src/test/` layer,
|
|
360
|
+
which runs against a real database, is right — the mocked suites are a supplement that became the
|
|
361
|
+
proof (landmine 11 of `docs/analysis-midday-2026-08.md`).
|
|
362
|
+
|
|
363
|
+
So **both consumers are the green fixtures**, and that is the whole reason `integrationDirs` and
|
|
364
|
+
`dbModules` are `requireOption`-gated with no defaults: a default drawn from two repos that do not
|
|
365
|
+
have the pattern would match nothing and report a clean tree it had never looked at — the exact
|
|
366
|
+
failure `no-unregistered-plugin-dir.manifests` was already caught by.
|
|
367
|
+
|
|
368
|
+
Three matching decisions, each with a case in the node layer:
|
|
369
|
+
|
|
370
|
+
- **the three spellings of one act** — `vi.mock`, `jest.mock`, `mock.module`. A repo on bun with
|
|
371
|
+
only `vi`/`jest` checked would be entirely unpoliced.
|
|
372
|
+
- **never a containment test.** `@repo/dbt` is not `@repo/db`, and `__tests__/integration-helpers/`
|
|
373
|
+
is not `__tests__/integration/`. Exact, subpath, or whole path segment.
|
|
374
|
+
- **`mockNames`, empty by default.** Midday's shape has no mock CALL to see: the handle is built in
|
|
375
|
+
a setup file and threaded through a context helper. A rule cannot infer that an identifier is a
|
|
376
|
+
mock, so a repo that has one names it — and a repo that does not gets no rule guessing at its
|
|
377
|
+
identifiers.
|
|
378
|
+
|
|
379
|
+
### The `backend-medusa` seven (1.0.0) — Medusa's own silences, turned into gates
|
|
380
|
+
### The `backend-medusa` ten (1.0.0) — Medusa's own silences, turned into gates
|
|
381
|
+
|
|
382
|
+
Every rule below exists because the framework **infers** something instead of making you say it.
|
|
383
|
+
None of them throws at runtime; all of them are found in production, far from the cause. Six were
|
|
384
|
+
scans in dielime's `packages/verify-arch` first and are now also per-file rules; two come from
|
|
385
|
+
Medusa's own source; one from an estimate a route already labels by hand. Three more — the per-file
|
|
386
|
+
halves of R4.3, R3.8 and R5.1 — landed after adoption showed what a text scan misses.
|
|
387
|
+
|
|
388
|
+
| rule | the silence | source |
|
|
389
|
+
|---|---|---|
|
|
390
|
+
| `mutating-step-declares-retry` | `maxRetries` defaults to **0** (`orchestration/transaction/types.ts:48`), so a transient failure reverts real state a retry would have absorbed | dielime scan R5.2 |
|
|
391
|
+
| `step-declares-compensation-or-none` | `stepConfig.noCompensation = !compensateFn` (`create-step.ts:184`) — a deliberate non-compensation and a forgotten one are the same source | analysis §1.1, "highest-value lint gap"; dielime scan R5.6 |
|
|
392
|
+
| `named-when-block` | an unnamed `when()` derives a `when-then-{ulid}` step name (`composer/when.ts:120-126`), so a resumed run cannot find its checkpoint | dielime scan R5.5 |
|
|
393
|
+
| `subscriber-declares-id` | the loader infers a subscriber id from a function or file name; two that infer alike dedupe each other | dielime scan R5.4 |
|
|
394
|
+
| `no-loader-side-effect-in-index-file` | `ResourceLoader.discoverResources` filters files named `index` (`resource-loader.js:51`) | dielime scan #15442 |
|
|
395
|
+
| `service-name-not-reserved` | a module key matching the `Modules` enum silently REPLACES the built-in (`medusa-app.ts:175-198`); `registerModule` guards only aliases and mains | analysis §1.3 — dielime's `catalog_search` scar |
|
|
396
|
+
| `no-index-count-as-exact` | the index engine's count is an EXPLAIN plan-row estimate and reads as an exact total | analysis §3a; dielime's b2b route labels it by hand |
|
|
397
|
+
| `unique-query-step-name` | the step-handler map is a plain assignment and the orchestrator appends a duplicate action with no dedupe, so two un-named query steps in one body last-win: one read never happens | dielime scan R4.3 |
|
|
398
|
+
| `plugin-route-namespaced` | the routes-loader assigns last-write-wins, so a plugin route with no namespace segment is claimable by any other package with no error | dielime scan R3.8 |
|
|
399
|
+
| `route-no-inline-mutation` | a route that writes directly succeeds, returns 200, and skips the compensation and retry class that live behind the workflow door | dielime scan R5.1 |
|
|
400
|
+
|
|
401
|
+
Four postures they share, all of them the one `emit-declares-attempts` set:
|
|
402
|
+
|
|
403
|
+
- **unreadable is not absent.** A config assembled from a spread, or held in a variable, passes —
|
|
404
|
+
the flag or the budget may be in the base, and a rule that guessed there would fire on every repo
|
|
405
|
+
that factored its step configs out.
|
|
406
|
+
- **a value the rule cannot evaluate passes.** `maxRetries: BUDGET` says nothing; only a literal is
|
|
407
|
+
judged, because a rule that demanded a literal would push every repo into inlining numbers.
|
|
408
|
+
- **every framework name is an option** — `stepFactories`, `blocks`, `moduleFactories`,
|
|
409
|
+
`resourceFactories`, `mutationPrefixes`, `estimateFields`, `labelKey`, `paths`, `directories`,
|
|
410
|
+
`reserved` — each with the framework's own value as its default, and each refusing the run when
|
|
411
|
+
emptied out.
|
|
412
|
+
- **arity, not shape, where the framework overloads.** `named-when-block` reads `when(input, cond)`
|
|
413
|
+
vs `when(name, input, cond)` by argument count. The first version of that rule read the FIRST
|
|
414
|
+
ARGUMENT'S TYPE and passed every real call site, because the input is a variable — the hand-built
|
|
415
|
+
test that caught it is `reports the unnamed overload`.
|
|
416
|
+
|
|
417
|
+
Two of them carry a decision worth naming:
|
|
418
|
+
|
|
419
|
+
**`no-loader-side-effect-in-index-file` defaults to `["subscribers/", "jobs/"]`, not all three
|
|
420
|
+
kinds** — measured, not assumed. In the installed 2.19 packages,
|
|
421
|
+
`framework/dist/utils/resource-loader.js:51` still filters `parsedName.name !== "index"` unless the
|
|
422
|
+
caller passes `allowIndex`, and `framework/dist/workflows/workflow-loader.js:34` is the **only**
|
|
423
|
+
caller in the whole framework that does (`grep -rn allowIndex` over the dist returns that call, the
|
|
424
|
+
parameter and its doc comment, and nothing else). So a `workflows/index.ts` **is** discovered on
|
|
425
|
+
2.19 and a `subscribers/index.ts` is not. A rule firing on a file the framework reads is a false
|
|
426
|
+
positive, and false positives are how a rule gets ignored — so the default is what was measured, and
|
|
427
|
+
a repo below 2.19 adds `workflows/` back. The `medusa` scanner pack in `@geonosis/verify-arch`
|
|
428
|
+
defaults the same way, and `examples/dielime.verify-arch.json` adds the third kind, which is what
|
|
429
|
+
keeps that repo's numbers identical to today's.
|
|
430
|
+
|
|
431
|
+
**The class an AST rule wins over a text scan.** These three are the per-file halves of scans a
|
|
432
|
+
consumer runs repo-wide, and carrying both is deliberate — but where they disagree, the rule is
|
|
433
|
+
right, and R5.6 is the case that proves what the disagreement costs.
|
|
434
|
+
|
|
435
|
+
That scan decided whether a step had a compensation function by counting top-level commas in the
|
|
436
|
+
`createStep(` call. The house style closes a multi-line call with a TRAILING comma, so a step with
|
|
437
|
+
two arguments read as three, and "has a compensation function" was true of every step that ended in
|
|
438
|
+
`,)`. The scan reported **0 for its whole life**, with five real violations behind it — two of which
|
|
439
|
+
became real compensations once someone could see them. `step-declares-compensation-or-none` was
|
|
440
|
+
immune from its first day, because an AST does not count commas: it is handed the arguments.
|
|
441
|
+
|
|
442
|
+
The same class shows up in each of the three added here. `route-no-inline-mutation` reads its
|
|
443
|
+
escape-hatch annotation from the STATEMENT, where the equivalent scan looks on the matching line or
|
|
444
|
+
the one above — and a consumer's own annotated write is spread over six lines by a cast, so the
|
|
445
|
+
annotation lands five lines from the call and the scan is red on a write a person already signed
|
|
446
|
+
off. `unique-query-step-name` tells a name it CANNOT EVALUATE from a name that is absent, which a
|
|
447
|
+
regex over `.config({ name: … })` cannot. And every one of them sees files the scan's collector
|
|
448
|
+
never hands over: a scan reads what its walk collected, a rule is handed every file that compiles.
|
|
449
|
+
|
|
450
|
+
None of that makes the scans redundant. A scan proves the invariant repo-wide in one command,
|
|
451
|
+
whatever the lint stack is doing that week, and it sees facts no per-file rule can — two packages
|
|
452
|
+
claiming one route, a `serviceName` that must be unique across every module. The split is the
|
|
453
|
+
decision rule, not a ranking: whole-graph facts are scans, per-file facts are rules, and the six
|
|
454
|
+
that are both are defence in depth on purpose.
|
|
455
|
+
|
|
456
|
+
**`step-declares-compensation-or-none` judges every step, not only mutating ones.** dielime's R5.6
|
|
457
|
+
scan restricts itself to steps whose text contains a write; the rule does not, because
|
|
458
|
+
`noCompensation = !compensateFn` is true of every step and a read-only step declares
|
|
459
|
+
`noCompensation: true` once and is done. The two are therefore not redundant: the scan is the
|
|
460
|
+
repo-wide backstop, the rule is the finer instrument. `noCompensation: false` is the inferred state
|
|
461
|
+
written out and is still reported.
|
|
462
|
+
|
|
463
|
+
### `store-route-scopes-tenant-data` (dielime only) — Medusa-specific
|
|
464
|
+
|
|
465
|
+
`/api/store/**/route.ts` is a Medusa convention. It is in `backend-medusa`, and also in
|
|
466
|
+
`tenant-scoping` because that is what it enforces.
|
|
467
|
+
|
|
468
|
+
### `no-logic-in-component-files` — dielime's narrowed version
|
|
469
|
+
|
|
470
|
+
during.day's version also reported type re-exports and custom hooks in component files. Both are
|
|
471
|
+
covered by dedicated rules in the union (`no-type-definitions-in-components`,
|
|
472
|
+
`no-hook-in-component-disguise`), and during.day's own suite does not test this rule. dielime's
|
|
473
|
+
narrower "one public component per file" version is the one under test, and the one kept.
|
|
474
|
+
|
|
475
|
+
### `tissue-must-compose` — dielime's, minus its backstop
|
|
476
|
+
|
|
477
|
+
dielime added a "renders ANY PascalCase tag" backstop. during.day has a test proving that backstop
|
|
478
|
+
lets a tissue satisfy the gate by rendering a component it imported as a **type** — precisely the
|
|
479
|
+
lint-theater the rule exists for. The backstop is dropped; dielime's broader `COMPOSITION_PATTERNS`
|
|
480
|
+
(feature barrels, `.parts`/`.sections` siblings, react-email primitives) already recognise real
|
|
481
|
+
arrangements, and dielime's fixtures stay green without it.
|
|
482
|
+
|
|
483
|
+
### `cell-must-not-compose-cell` — the union's two halves collided on the barrel
|
|
484
|
+
|
|
485
|
+
The kit's version is dielime's scope (`(?:^|/)cells/`, so the rule reaches a `packages/ui` tree with
|
|
486
|
+
no `features/<name>/` segment) plus during.day's visitors (`ExportAllDeclaration` and
|
|
487
|
+
`ExportNamedDeclaration`, so a cell cannot launder an import through `export … from`). Each half is
|
|
488
|
+
right. Together, unguarded, they reported every `cells/index.ts` barrel — because a barrel is
|
|
489
|
+
nothing but `export … from` lines naming sibling cells. dielime's first adoption produced 32 such
|
|
490
|
+
errors, and every one of them was a file obeying `cells-folder-index-is-barrel`, which is also an
|
|
491
|
+
error and demands exactly that shape.
|
|
492
|
+
|
|
493
|
+
Neither fork could see it:
|
|
494
|
+
|
|
495
|
+
- **dielime** had the broad scope but visited `ImportDeclaration` only, so the re-exports in its
|
|
496
|
+
barrels were invisible to the rule;
|
|
497
|
+
- **during.day** visited all three, but scoped to `features/[^/]+/cells/`, and it has no
|
|
498
|
+
`cells/index.ts` or `cells/dynamic.ts*` anywhere in `apps/` or `packages/` — nothing to collide
|
|
499
|
+
with.
|
|
500
|
+
|
|
501
|
+
So: `cell-must-not-compose-cell` returns an empty visitor for the barrel files
|
|
502
|
+
`cells-folder-index-is-barrel` sanctions. A real cell importing — or re-exporting — another cell
|
|
503
|
+
still fires. The list of barrel filenames lives once, in `src/rules/lib/cells-barrel.ts`, and both
|
|
504
|
+
rules read it; two copies of that list is how two rules came to demand opposite things of one file.
|
|
505
|
+
Closing it also fixed a hole in the barrel rule's own pattern, which required a leading `/` and
|
|
506
|
+
therefore no-op'd on the cwd-relative `cells/index.ts` oxlint reports for a package whose own root
|
|
507
|
+
holds `cells/`.
|
|
508
|
+
|
|
509
|
+
### `documents-share-one-table` — matches `pgTable` as well as `sqliteTable`
|
|
510
|
+
|
|
511
|
+
during.day's fork matched `literalNamedBy(node, 'sqliteTable')` and nothing else, and every tenant
|
|
512
|
+
table on that tree has been `pgTable` since its plan 037. Its own probe: a well-formed per-kind
|
|
513
|
+
table
|
|
514
|
+
|
|
515
|
+
```ts
|
|
516
|
+
export const invoices = pgTable('invoices', { id: text('id').primaryKey(), orgId: orgIdColumn() })
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
under `packages/db/src/schema/tenant/` produced **zero** findings from this rule; only the malformed
|
|
520
|
+
variant was caught, by `tenant-tables-carry-org-id`, and by accident. The fork's rule had been dead
|
|
521
|
+
since the tenant plane moved to Postgres.
|
|
522
|
+
|
|
523
|
+
The kit matching both constructors is therefore a fix, not a widening for its own sake. It stands
|
|
524
|
+
because the rule is about the second storage a base entity retired — which is true whatever the
|
|
525
|
+
dialect — and because `dialect-through-the-seam` already says the dialect is a seam fact rather than
|
|
526
|
+
a thing a document kind gets to have an opinion about. Both branches are exercised through the real
|
|
527
|
+
binary in `src/rules/__fixtures__/packages/db/src/schema/tenant/`, so neither can go quiet again.
|
|
528
|
+
|
|
529
|
+
### `no-raw-html-atoms` — the tier it was always documented for (0.3.0)
|
|
530
|
+
|
|
531
|
+
Its own description said levels 2-6 and it fired at every level, `atoms/` included, where a raw
|
|
532
|
+
`<button>` is not a violation but the whole point of the file. dielime armed it by switching the rule
|
|
533
|
+
off over `**/atoms/**` in its own config: a repo carrying the kit's defect as configuration.
|
|
534
|
+
|
|
535
|
+
It is now silent inside the atom tier — `(?:^|/)(?:features/[^/]+/)?atoms/`, the merged pattern from
|
|
536
|
+
`src/rules/lib/tiers.ts`, which reaches a flat kit as well as a feature-sliced app.
|
|
537
|
+
|
|
538
|
+
**The default is not an allow-list of the five upper tiers**, and that is the load-bearing decision.
|
|
539
|
+
A real tree has files in no tier folder at all — a Medusa admin widget, a Next route segment, a
|
|
540
|
+
one-off surface — and the atom exists there too. Scoping to the five named tiers would have dropped
|
|
541
|
+
about a quarter of one consumer's findings in the same commit that called itself a fix, which is a
|
|
542
|
+
rule getting quieter with a fix's clothes on. So: **everywhere except the atom tier**, and a repo that
|
|
543
|
+
wants less says so:
|
|
544
|
+
|
|
545
|
+
```jsonc
|
|
546
|
+
"biological-architecture/no-raw-html-atoms": [
|
|
547
|
+
"error",
|
|
548
|
+
[{ "element": "button", "atom": "Button", "importPath": "@kit/ui/atoms/button" }],
|
|
549
|
+
{ "scope": ["cells", "tissues"] }
|
|
550
|
+
]
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
A `scope` naming something that is not a tier stops the run rather than matching nothing — the inert
|
|
554
|
+
rule the option refusal exists to stop, one option along.
|
|
555
|
+
|
|
556
|
+
### Other repo-specific literals turned into options
|
|
557
|
+
|
|
558
|
+
| Rule | Option | Unconfigured |
|
|
559
|
+
|---|---|---|
|
|
560
|
+
| `organelle-dependency` | `sharedFeatures` | `['shared']` — during.day's `charts`/`document` are config, not law |
|
|
561
|
+
| `mutating-step-declares-retry` | `retryKey` | `maxRetries` — Medusa's key. `@geonosis/workflows` spells the same budget `retry: { limit, backoff }` (`create-step.ts`), and with the key compiled in a correctly budgeted step on that engine was a finding for ever |
|
|
562
|
+
| `mutating-step-declares-retry` | `retryLimitKey` | `limit` — the key that holds the NUMBER when the budget is an object. #191: reading only the bare form let `retry: { limit: 0 }` pass, which is the same silence as `maxRetries: 0` |
|
|
563
|
+
| `documents-share-one-table` | `perKindTables` | **refuses the run** |
|
|
564
|
+
| `worker-handles-are-scoped` | `within` + `wrapper` | **refuses the run** — `withCells` and `/apps/api/src/` were compiled in, so the rule served one repo's layout and one repo's vocabulary and a scope opener called anything else read as unscoped for ever |
|
|
565
|
+
| `subpath-over-barrel` | `packages` (`{ name, subpaths: { <subpath>: string[] } }[]`) | **refuses the run** — which of a repo's own packages expose subpaths, and which names live under each, is a map only that package's owner has |
|
|
566
|
+
| `durable-body-reads-through-steps` | `ports` | **refuses the run** — what a body reaches the outside world through is the repo's vocabulary; `durableKey` / `durableValue` (default `durable: true`) say how a workflow declares itself durable |
|
|
567
|
+
| `document-sagas-are-generic` | `perKindPrefixes` | **refuses the run** |
|
|
568
|
+
| `document-sagas-are-generic` | `within` | **refuses the run** — `/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 |
|
|
569
|
+
| `document-sagas-are-generic` | `factories` | `['saga', 'createWorkflow']` — the callee was `saga` alone, so a per-entity `createWorkflow('invoice.create', …)` produced no finding (026-W2, measured). A name assembled from the entity is a template literal the rule cannot read: that is the mint the rule sends people to, not a gap in it |
|
|
570
|
+
| `unique-query-step-name` | `queryStepCalls` | **refuses the run** — the two names it shipped with are Medusa framework calls with no counterpart, so on the org engine the rule had zero reach while reading as enforced (026-W2, measured through the binary). There is no second spelling to inherit: `@geonosis/workflows` has no query-step primitive, and a repeated step name there is DEFINED rather than lost — `reserve`, `reserve#2` in call order (`engine.ts:124`) |
|
|
571
|
+
| `no-raw-html-atoms` | (positional) element→atom map | **refuses the run** |
|
|
572
|
+
| `no-raw-html-atoms` | `scope` (second positional, `{ scope: ["cells", …] }`) | every file except the atom tier |
|
|
573
|
+
|
|
574
|
+
Package specifiers in diagnostics (`@dielime/ui`, `@during/utils/time`) were made generic, and
|
|
575
|
+
`CLAUDE.md §…` citations were removed: they pointed at a law file this package does not ship, and
|
|
576
|
+
the remedy now lives in each rule's `fixShape`.
|
|
577
|
+
|
|
578
|
+
### The rule core (0.4.0) — `src/rules/lib/`
|
|
579
|
+
|
|
580
|
+
Measured on 2026-08-29, before any of it existed: **37 of 54 rules normalised the filename
|
|
581
|
+
themselves**, in three spellings, one of which (`replace(/\\/g, …)` without the global flag in an
|
|
582
|
+
earlier fork) fixed only the first separator. **21 parsed an import source themselves**, and each
|
|
583
|
+
covered a different subset of the five ways a file can name another. **11 hand-wrote the same
|
|
584
|
+
re-export visitor.** None of that was decided; it was the order the rules were written in, and one
|
|
585
|
+
fix to how a path is read had to be found rule by rule.
|
|
586
|
+
|
|
587
|
+
| module | what it is the one door for |
|
|
588
|
+
|---|---|
|
|
589
|
+
| `lib/paths.ts` | `normalise` (backslashes → `/`), `fileOf` (the last segment, as a diagnostic names it) |
|
|
590
|
+
| `lib/tiers.ts` | `TIERS`, `inTier`, `tierOf`, `featureOf`, `requireTiers` — the ONE classification |
|
|
591
|
+
| `lib/imports.ts` | `sourceOf` (import · `export … from` · `export * from` · `import()` · `require()`), `isTypeOnly`, `resolveRelative` |
|
|
592
|
+
| `lib/visitors.ts` | `onImportSource` — one visitor set over all five shapes — and `mergeVisitors` |
|
|
593
|
+
| `lib/direction-rule.ts` | `directionRule()`: a direction rule as a declaration (scope, forbidden, messages) |
|
|
594
|
+
| `lib/options.ts` | `requireOption` — an enabled rule that cannot fire refuses the run |
|
|
595
|
+
| `lib/cells-barrel.ts` | the barrel list two rules must agree on |
|
|
596
|
+
| `lib/literal-call.ts` | the string a `saga('x', …)`-shaped call names |
|
|
597
|
+
|
|
598
|
+
The **Core** column in the preset table below says which of these each rule stands on. `paths` alone
|
|
599
|
+
means the rule reads a filename and nothing else; `directionRule` means the rule file is a
|
|
600
|
+
declaration and holds no mechanism at all.
|
|
601
|
+
|
|
602
|
+
`onImportSource` visits a type-only import only when the rule counts one. Four do:
|
|
603
|
+
`no-orm-outside-db` and `dialect-through-the-seam` say why in their docs (a type borrowed from the
|
|
604
|
+
ORM is a line a replacement would still have to rewrite), and `atom-no-deps` and
|
|
605
|
+
`molecule-atoms-only` inherited it from dielime — an atom that borrows a domain's type is an atom
|
|
606
|
+
that knows a domain. The other six direction rules skip them, as they always did.
|
|
607
|
+
|
|
608
|
+
### Direction rules resolve relative imports (0.4.0)
|
|
609
|
+
|
|
610
|
+
**Before:** `cell-must-not-compose-cell` and `layer-walls` resolved a relative source against the
|
|
611
|
+
importing file before judging it. The other eight direction rules read the string as written, so
|
|
612
|
+
`../../billing/organelles/totals` — which names no feature and no tier until it is resolved — was
|
|
613
|
+
invisible to them. Whether a relative climb across a feature boundary was a finding depended on
|
|
614
|
+
which rule happened to look at it. during.day's A2b probe found this; it was one resolver in two
|
|
615
|
+
rules and no resolver in eight.
|
|
616
|
+
|
|
617
|
+
**Now:** the classification every direction rule judges by goes through `resolveRelative` once, in
|
|
618
|
+
the factory. Where an import LANDS is the fact; how it was spelled is what the message prints.
|
|
619
|
+
|
|
620
|
+
**Corrected in 0.5.0, by measurement.** "Ten rules now resolve relative imports" is true of the
|
|
621
|
+
mechanism and misleading about the effect: resolution changes the ANSWER for only the rules whose
|
|
622
|
+
forbidden pattern is **feature-anchored** — `features/<name>/stores/`, `features/<name>/organelles/`
|
|
623
|
+
— because a relative climb carries no `features/` segment until it is resolved. The other seven
|
|
624
|
+
forbid an unanchored substring (`stores/`, `cells/`, `/organelles/`, `molecules/`) which a relative
|
|
625
|
+
source contains as written, so they caught the relative spelling before 0.4.0 and after it alike.
|
|
626
|
+
0.3.0 against 0.5.0 over a corpus of relative-only cases, one per rule, in
|
|
627
|
+
`proofs/021-relative-spellings/`: three rules answer differently, and one of the three is 0.5.0's own
|
|
628
|
+
new one. Two rules changed in 0.4.0, which is exactly what 0.4.0's parity table measured.
|
|
629
|
+
|
|
630
|
+
**What 0.5.0 added is reach evidence, not behaviour.** Every direction rule now carries BOTH
|
|
631
|
+
spellings in the shipped corpus — the aliased one and a relative climb — so a consumer who meets one
|
|
632
|
+
of them has a fixture to check their install against. `no-orm-outside-db` is the exception: its
|
|
633
|
+
target is a bare package specifier, so a relative case is not expressible for it.
|
|
634
|
+
|
|
635
|
+
This is a **widening**, and it is the only behaviour change in K1.2. Its scope is measured, not
|
|
636
|
+
argued — `proofs/018-T3-relative-imports/`:
|
|
637
|
+
|
|
638
|
+
| tree | A | B | lost | gained |
|
|
639
|
+
|---|---:|---:|---:|---:|
|
|
640
|
+
| the kit's corpus | 99 | 101 | 0 | 2 — `no-cross-feature-stores` and `organelle-dependency`, on the two fixtures written RED for this |
|
|
641
|
+
| dielime, its own config, `apps packages` | 135 | 135 | 0 | 0 |
|
|
642
|
+
|
|
643
|
+
dielime's zero is not luck: that tree has seven relative cross-feature imports of a store or an
|
|
644
|
+
organelle, five of them into `shared/organelles/*` (which `organelle-dependency`'s `sharedFeatures`
|
|
645
|
+
option permits) and two of them in **cells** reaching another feature's organelle — which no rule
|
|
646
|
+
here polices, because `organelle-dependency` is scoped to `organelles/` and `no-cross-feature-stores`
|
|
647
|
+
only looks at stores. That gap is real and is not this decision's to close — it is closed in 0.5.0
|
|
648
|
+
by `cell-no-cross-feature-organelles`, below.
|
|
649
|
+
|
|
650
|
+
### `cell-no-cross-feature-organelles` (written here, from the gap above) — 0.5.0
|
|
651
|
+
|
|
652
|
+
The boundary the two rules above draw from two sides had a hole in the middle. `organelle-dependency`
|
|
653
|
+
is scoped to `organelles/`: it stops an organelle reaching sideways into another feature's organelle,
|
|
654
|
+
and says nothing about who imports it. `no-cross-feature-stores` is scoped to cells AND organelles,
|
|
655
|
+
but only judges `features/<x>/stores/`. So a **cell** importing another feature's **organelle** — a
|
|
656
|
+
hook holding that feature's state, pulled across the boundary — passed both.
|
|
657
|
+
|
|
658
|
+
It is not hypothetical. dielime, on its own config, with the rule enabled through a temporary config
|
|
659
|
+
and nothing else changed:
|
|
660
|
+
|
|
661
|
+
| site | import |
|
|
662
|
+
|---|---|
|
|
663
|
+
| `apps/web/src/features/site/cells/site-chrome.tsx:27` | `../../checkout/organelles/use-payment-providers-effect` |
|
|
664
|
+
| `apps/web/src/features/site/cells/site-chrome.tsx:28` | `../../search/organelles/search-autocomplete` |
|
|
665
|
+
| `apps/web/src/features/cart/cells/cart-view.tsx:12` | `../../checkout/organelles/use-payment-providers-effect` |
|
|
666
|
+
|
|
667
|
+
Three findings in two files — the 0.4.0 note above said "two", counting files rather than imports.
|
|
668
|
+
All three are relative climbs, so none of them would have been visible before 0.4.0's widening; five
|
|
669
|
+
further climbs in that tree land in `shared/organelles/*` and are allowed, which is what the
|
|
670
|
+
`sharedFeatures` option (default `['shared']`, the same default and the same meaning as
|
|
671
|
+
`organelle-dependency`'s) is for.
|
|
672
|
+
|
|
673
|
+
The rule is a `directionRule()` declaration: scope is the `cells` tier pattern, so it reaches a
|
|
674
|
+
feature-sliced app and a flat kit alike, and the forbidden target is an organelle of a feature that
|
|
675
|
+
is neither the importer's own nor named in `sharedFeatures`. Relative and aliased sources are judged
|
|
676
|
+
the same way, because the factory resolves before it judges. Preset: `frontend`.
|
|
677
|
+
|
|
678
|
+
## Presets
|
|
679
|
+
|
|
680
|
+
`recommended` = `frontend`. `themekit` exists because both its rules are **inert until a repo says
|
|
681
|
+
how its own tokens are spelled** — the custom-property prefix and the font role names are that
|
|
682
|
+
repo's data, and either one guessed at would be one stylesheet wearing a preset's name. It is also
|
|
683
|
+
the preset a repo can want without wanting a tier vocabulary: a component library conforms to a
|
|
684
|
+
theme whether or not it is organised biologically. `backend-d1` exists because `no-d1-transaction` and
|
|
685
|
+
`worker-handles-are-scoped` are **false** on any other engine — a generic backend preset carrying
|
|
686
|
+
them would be a repo fact wearing a preset's name. `backend-workflows-cf` exists for the same reason:
|
|
687
|
+
`step-opens-its-own-cell` is true of a durable run on Cloudflare Workflows and false of an inline
|
|
688
|
+
one. It was called `backend-sagaflow-cf` until 1.5.0, after the engine it was written for; the old
|
|
689
|
+
name resolves to the same rules for ONE release and says so once, then goes. `package-subpaths` exists because `subpath-over-barrel` is inert until a repo writes out which
|
|
690
|
+
of ITS packages expose subpaths and which names live under each — a map only that package's owner
|
|
691
|
+
has, and one a kit that guessed at it would keep stale on somebody else's behalf.
|
|
692
|
+
|
|
693
|
+
| Rule | Preset(s) | Requires | Core | In dielime | In during.day | Same code |
|
|
694
|
+
|---|---|---|---|---|---|---|
|
|
695
|
+
| `atom-no-deps` | frontend | — | `directionRule` | yes | yes | diverged |
|
|
696
|
+
| `boundary-time-is-primitive` | backend-workflows | `types` | `nodes` + `paths` + `options` | — | — | **written here** (D-050, #145) |
|
|
697
|
+
| `cell-must-be-stateful` | frontend | — | `paths` | yes | — | dielime only |
|
|
698
|
+
| `cell-must-not-compose-cell` | frontend | — | `directionRule` + `cells-barrel` | yes | yes | diverged |
|
|
699
|
+
| `cell-no-cross-feature-organelles` | frontend | — | `directionRule` | — | — | **written here** |
|
|
700
|
+
| `cell-no-tissues` | frontend | — | `directionRule` | yes | yes | diverged |
|
|
701
|
+
| `cells-folder-index-is-barrel` | frontend | — | `paths` + `cells-barrel` | yes | yes | diverged |
|
|
702
|
+
| `compound-must-be-stateless` | frontend | — | `paths` | yes | yes | diverged |
|
|
703
|
+
| `compound-no-stores` | frontend | — | `directionRule` | yes | yes | diverged |
|
|
704
|
+
| `constants-in-constants-file` | backend-workflows | — | `paths` | — | yes | during only |
|
|
705
|
+
| `dialect-through-the-seam` | backend-workflows | — | `onImportSource` + `mergeVisitors` + `paths` | — | yes | during only |
|
|
706
|
+
| `document-sagas-are-generic` | backend-workflows | `perKindPrefixes` | `paths` + `options` + `literal-call` | — | yes | during only |
|
|
707
|
+
| `documents-share-one-table` | backend-workflows | `perKindTables` | `paths` + `options` + `literal-call` | — | yes | during only |
|
|
708
|
+
| `durable-body-reads-through-steps` | backend-workflows | `ports` | `nodes` + `objects` + `steps` + `options` | — | — | **written here** (#167, design-026 §8) |
|
|
709
|
+
| `effect-hook-naming` | frontend | — | `paths` | yes | yes | diverged |
|
|
710
|
+
| `emit-declares-attempts` | backend-medusa | — | `options` | — | — | **written here** |
|
|
711
|
+
| `font-roles-only` | themekit | `roles` | `paths` + `options` + `styling` | — | — | **written here** (U2) |
|
|
712
|
+
| `layer-walls` | tooling | `layers` | `onImportSource` + `imports` + `paths` + `options` | — | — | **written here** |
|
|
713
|
+
| `max-comment-density` | comment-ceiling | — | `paths` | — | yes | during only |
|
|
714
|
+
| `molecule-atoms-only` | frontend | — | `directionRule` | yes | yes | diverged |
|
|
715
|
+
| `molecule-must-compose` | frontend | — | — | yes | yes | diverged |
|
|
716
|
+
| `mutating-step-declares-retry` | backend-medusa + backend-workflows | — | `nodes` + `objects` + `steps` + `options` | — | — | **written here** (dielime scan R5.2) |
|
|
717
|
+
| `named-when-block` | backend-medusa | — | `nodes` + `options` | — | — | **written here** (dielime scan R5.5) |
|
|
718
|
+
| `next-route-segment-is-thin-delegate` | frontend | — | `paths` | yes | yes | diverged |
|
|
719
|
+
| `no-unregistered-plugin-dir` | integrations | `roots` | `options` + `paths` | — | — | **written here** (Midday's 8 dead dirs) |
|
|
720
|
+
| `no-mock-db-in-integration` | testbed | `integrationDirs` | `options` + `paths` | — | — | **written here** (Midday's 35 mocked router suites) |
|
|
721
|
+
| `router-schema-parity` | backend-trpc | `routers` | `options` + `paths` | — | — | **written here** (during.day's mirror) |
|
|
722
|
+
| `no-brand-names` | frontend | `brands` | `paths` + `options` | yes | — | dielime only |
|
|
723
|
+
| `no-card-shaped-div` | frontend | — | — | yes | yes | diverged |
|
|
724
|
+
| `no-cross-feature-stores` | frontend | — | `directionRule` | yes | yes | diverged |
|
|
725
|
+
| `no-d1-transaction` | backend-d1 | — | `paths` | — | yes | during only |
|
|
726
|
+
| `no-duplicate-jsx-patterns` | frontend | — | `paths` | yes | yes | diverged |
|
|
727
|
+
| `no-hook-in-component-disguise` | frontend | — | `paths` | yes | yes | diverged |
|
|
728
|
+
| `no-index-count-as-exact` | backend-medusa | — | `nodes` + `objects` + `options` | — | — | **written here** (Medusa source) |
|
|
729
|
+
| `no-inert-hidden-jsx` | frontend | — | `paths` | yes | yes | diverged |
|
|
730
|
+
| `no-inline-data-in-jsx` | frontend | — | `paths` | yes | yes | diverged |
|
|
731
|
+
| `no-invalid-feature-folders` | frontend | — | `paths` | yes | yes | diverged |
|
|
732
|
+
| `no-loader-side-effect-in-index-file` | backend-medusa | — | `nodes` + `paths` + `options` | — | — | **written here** (dielime scan #15442) |
|
|
733
|
+
| `no-logic-in-component-files` | frontend | — | `paths` | yes | yes | diverged |
|
|
734
|
+
| `plugin-route-namespaced` | backend-medusa | `packages` | `paths` + `options` | — | — | **written here** (dielime scan R3.8) |
|
|
735
|
+
| `no-orm-outside-db` | backend-workflows | — | `directionRule` | — | yes | during only |
|
|
736
|
+
| `no-raw-html-atoms` | frontend | `element→atom map` | `tiers` + `options` | yes | yes | diverged |
|
|
737
|
+
| `no-raw-sql-outside-allowed` | backend-workflows | — | `paths` | — | yes | during only |
|
|
738
|
+
| `no-react-namespace` | frontend | — | — | yes | yes | **identical** |
|
|
739
|
+
| `no-renamed-html-props` | frontend | — | — | yes | yes | **identical** |
|
|
740
|
+
| `no-render-prop-reader` | frontend | — | `paths` | yes | yes | diverged |
|
|
741
|
+
| `no-trivial-wrapper-component` | frontend | — | `paths` | yes | yes | diverged |
|
|
742
|
+
| `no-ts-in-bio-folders` | frontend | — | `paths` | yes | yes | diverged |
|
|
743
|
+
| `no-type-definitions-in-components` | frontend | — | `paths` | yes | yes | diverged |
|
|
744
|
+
| `no-void-port` | backend-workflows | — | — | — | yes | during only |
|
|
745
|
+
| `organelle-dependency` | frontend | — | `directionRule` | yes | yes | diverged |
|
|
746
|
+
| `organelle-single-source` | frontend | — | `onImportSource` + `mergeVisitors` + `paths` | yes | yes | diverged |
|
|
747
|
+
| `queries-require-org-scope` | tenant-scoping | — | `paths` | — | yes | during only |
|
|
748
|
+
| `queue-loop-is-the-library` | backend-workflows | — | — | — | yes | during only |
|
|
749
|
+
| `route-no-inline-mutation` | backend-medusa | — | `nodes` + `paths` + `comments` + `options` | — | — | **written here** (dielime scan R5.1) |
|
|
750
|
+
| `service-name-not-reserved` | backend-medusa | — | `nodes` + `objects` + `options` | — | — | **written here** (Medusa source) |
|
|
751
|
+
| `ssot-no-inline-facts` | backend-workflows | — | `paths` | — | yes | during only |
|
|
752
|
+
| `ssot-no-process-env` | backend-workflows | — | `paths` | — | yes | during only |
|
|
753
|
+
| `step-declares-compensation-or-none` | backend-medusa + backend-workflows | — | `nodes` + `objects` + `steps` + `options` | — | — | **written here** (Medusa source) |
|
|
754
|
+
| `step-opens-its-own-cell` | backend-workflows-cf | `within` | `nodes` + `objects` + `paths` + `options` | — | yes | during only, re-targeted |
|
|
755
|
+
| `store-route-scopes-tenant-data` | backend-medusa + tenant-scoping | — | `paths` | yes | — | dielime only |
|
|
756
|
+
| `subpath-over-barrel` | package-subpaths | `packages` | `nodes` + `options` | — | — | **written here** (#180, during.day asked) |
|
|
757
|
+
| `subscriber-declares-id` | backend-medusa | — | `objects` + `paths` + `options` | — | — | **written here** (dielime scan R5.4) |
|
|
758
|
+
| `tables-declare-their-plane` | backend-workflows + tenant-scoping | — | `paths` + `literal-call` | — | yes | during only |
|
|
759
|
+
| `tenant-tables-carry-org-id` | tenant-scoping | — | `paths` + `literal-call` | — | yes | during only |
|
|
760
|
+
| `tokens-only-colors` | themekit | `tokenPrefix` | `paths` + `options` + `styling` + `colors` | — | — | **written here** (U2) |
|
|
761
|
+
| `time-through-the-door` | backend-workflows | `door` | `paths` + `options` | — | yes | during only, de-hardcoded |
|
|
762
|
+
| `tissue-must-compose` | frontend | — | `paths` | yes | yes | diverged |
|
|
763
|
+
| `tissue-no-data-props` | frontend | — | `paths` | yes | yes | diverged |
|
|
764
|
+
| `tissue-no-hooks` | frontend | — | `paths` | yes | yes | diverged |
|
|
765
|
+
| `tissue-no-organelles` | frontend | — | `directionRule` | yes | yes | diverged |
|
|
766
|
+
| `tissue-no-stores` | frontend | — | `directionRule` | yes | yes | diverged |
|
|
767
|
+
| `todo-names-its-plan` | tooling | `markers` | `comments` + `options` | — | — | **written here** (W28) |
|
|
768
|
+
| `unique-query-step-name` | backend-medusa | `queryStepCalls` | `nodes` + `objects` + `options` | — | — | **written here** (dielime scan R4.3) |
|
|
769
|
+
| `worker-handles-are-scoped` | backend-d1 | `within` | `paths` + `options` | — | yes (as `cell-handles-are-scoped`) | during only, de-hardcoded |
|
|
770
|
+
| `zustand-v5-best-practices` | frontend | — | — | yes | yes | **identical** |
|
|
771
|
+
|
|
772
|
+
Preset sizes: frontend 36 · backend-workflows 16 · backend-medusa 12 · tenant-scoping 4 ·
|
|
773
|
+
backend-d1 2 · tooling 2 · themekit 2 · backend-workflows-cf 1 · backend-trpc 1 · integrations 1 ·
|
|
774
|
+
package-subpaths 1 · testbed 1 · comment-ceiling 1 (+ `capitalized-comments`, which is oxlint's own
|
|
775
|
+
and only makes sense alongside the density rule).
|
|
776
|
+
|
|
777
|
+
`backend-workflows` and `backend-medusa` OVERLAP by two: `mutating-step-declares-retry` and
|
|
778
|
+
`step-declares-compensation-or-none` reach both engines' spellings — measured in 026-W2, and true of
|
|
779
|
+
the retry rule only since its key became an option — so both presets carry them. Moving either out
|
|
780
|
+
of `backend-medusa` would take a live rule away from the repo it was written for, which is a
|
|
781
|
+
downgrade wearing a preset change. `unique-query-step-name` stays Medusa's alone: the collision it
|
|
782
|
+
reports is that framework's step-handler map last-winning, and a repeated step name on the org
|
|
783
|
+
engine is DEFINED rather than lost (`reserve`, `reserve#2`, `engine.ts:124`).
|
|
784
|
+
|
|
785
|
+
## The build
|
|
786
|
+
|
|
787
|
+
Two builds existed. during.day's `package.json` resolves `.` through a `bun` condition to
|
|
788
|
+
`src/index.ts` and only falls back to `dist/index.js`; without bun, oxlint gets the fallback anyway.
|
|
789
|
+
dielime's is the plain one — `tsup … --format esm --no-dts --platform node --out-dir dist` with
|
|
790
|
+
`"exports": { ".": "./dist/index.js" }` — and that is what this package uses.
|
|
791
|
+
|
|
792
|
+
It is **source-proven**, not assumed: `src/rules/fixtures.test.ts` runs the real `oxlint` binary with
|
|
793
|
+
`jsPlugins: ["@geonosis/oxlint-plugin-biological-architecture"]` over the fixture tree and asserts
|
|
794
|
+
first that *something* fired. A plugin oxlint cannot load reports nothing, and every "should NOT
|
|
795
|
+
fire" case would pass silently without that assertion.
|
|
796
|
+
|
|
797
|
+
## `fixShape`
|
|
798
|
+
|
|
799
|
+
Every rule exports `{ meta, create, fixShape }`. `fixShape` is a short Markdown string in the voice
|
|
800
|
+
of during.day's `during-code` skill: what the gate rejects and what to write instead.
|
|
801
|
+
`pnpm generate:skill` assembles them into `skills/geonosis-code/SKILL.md`, grouped by preset. A
|
|
802
|
+
staleness test fails when the committed skill is not what the rules generate, so the skill and the
|
|
803
|
+
rules cannot drift.
|
|
804
|
+
|
|
805
|
+
## `probe`
|
|
806
|
+
|
|
807
|
+
The twenty-six rules whose reach depends on an option also export `probe(options)`, beside `fixShape`. It
|
|
808
|
+
returns `{ path, source }` — the smallest file that MUST fire the rule under the options it was
|
|
809
|
+
handed, at a path satisfying every PATH option the rule reads — or `{ files: [{ path, source }, …] }`
|
|
810
|
+
when one file cannot be enough. A single-file probe stays the bare form; readers accept both, and
|
|
811
|
+
must, because a consumer runs whatever plugin build it has installed.
|
|
812
|
+
|
|
813
|
+
It exists because a rule whose REACH is an option can never fire on a FIXED corpus. The fixtures
|
|
814
|
+
here say `acme` and `corpus/workflows/`; a repo says its own brand and `packages/workflows/src/`,
|
|
815
|
+
so `no-brand-names` and `step-opens-its-own-cell` read as firing nowhere in the two trees that
|
|
816
|
+
enforce them on every file — which is what both source repos reported against themselves on
|
|
817
|
+
2026-08-30. `@geonosis/doctor`'s `exercised` check reads `probe` off the plugin a config loads,
|
|
818
|
+
writes it into a copy of the corpus, and runs again.
|
|
819
|
+
|
|
820
|
+
A probe is a CLAIM, so `src/probes.test.ts` tries to break every one of them through the real
|
|
821
|
+
binary, under consumer-shaped options, and requires exactly one finding. Two rules deserve the
|
|
822
|
+
detail:
|
|
823
|
+
|
|
824
|
+
- `no-unregistered-plugin-dir` reads its registry off disk, so its probe fires only when the
|
|
825
|
+
configured registry is also in the tree. One file cannot be both the plugin directory and the
|
|
826
|
+
registry, so its probe declares BOTH — the directory under `roots`, and a registry carrying no
|
|
827
|
+
string literal at all, so nothing in it can name the directory beside it. It was a single file
|
|
828
|
+
until 2026-08-30 and fired nothing, which was the truth about the probe and a lie about every
|
|
829
|
+
consumer: dielime enforces this rule at error over four registries and read as a permanent FAIL
|
|
830
|
+
for it.
|
|
831
|
+
- `layer-walls` and the path-regex options are inverted by `lib/probe.ts`'s `pathMatching`, which
|
|
832
|
+
builds a path back out of the pattern and then TESTS it against that pattern, refusing with a
|
|
833
|
+
sentence when the inversion does not hold. It is deliberately not a general regex inverse, and it
|
|
834
|
+
is never trusted.
|
|
835
|
+
|
|
836
|
+
<!-- GENERATED: the options each rule accepts -->
|
|
837
|
+
|
|
838
|
+
## The options each rule accepts
|
|
839
|
+
|
|
840
|
+
One line per rule, from the rule’s own schema — the shape oxlint validates a config against.
|
|
841
|
+
Which of them a rule REFUSES to run without is the `Requires` column of the inventory above; an
|
|
842
|
+
option not named here is one no rule reads.
|
|
843
|
+
|
|
844
|
+
| Rule | Options it accepts |
|
|
845
|
+
|---|---|
|
|
846
|
+
| `atom-no-deps` | — |
|
|
847
|
+
| `boundary-time-is-primitive` | `doors`, `types` |
|
|
848
|
+
| `cell-must-be-stateful` | — |
|
|
849
|
+
| `cell-must-not-compose-cell` | — |
|
|
850
|
+
| `cell-no-cross-feature-organelles` | `sharedFeatures` |
|
|
851
|
+
| `cell-no-tissues` | — |
|
|
852
|
+
| `cells-folder-index-is-barrel` | — |
|
|
853
|
+
| `compound-must-be-stateless` | — |
|
|
854
|
+
| `compound-no-stores` | — |
|
|
855
|
+
| `constants-in-constants-file` | — |
|
|
856
|
+
| `dialect-through-the-seam` | — |
|
|
857
|
+
| `document-sagas-are-generic` | `factories`, `perKindPrefixes`, `within` |
|
|
858
|
+
| `documents-share-one-table` | `perKindTables` |
|
|
859
|
+
| `durable-body-reads-through-steps` | `durableKey`, `durableValue`, `ports`, `stepFactories`, `workflowFactories` |
|
|
860
|
+
| `effect-hook-naming` | — |
|
|
861
|
+
| `emit-declares-attempts` | `budgetHelpers`, `emitters` |
|
|
862
|
+
| `font-roles-only` | `allow`, `prefix`, `roles` |
|
|
863
|
+
| `layer-walls` | `externals`, `layers`, `typeOnly` |
|
|
864
|
+
| `max-comment-density` | — |
|
|
865
|
+
| `molecule-atoms-only` | — |
|
|
866
|
+
| `molecule-must-compose` | — |
|
|
867
|
+
| `mutating-step-declares-retry` | `mutationPrefixes`, `retryKey`, `retryLimitKey`, `stepFactories` |
|
|
868
|
+
| `named-when-block` | `blocks` |
|
|
869
|
+
| `next-route-segment-is-thin-delegate` | — |
|
|
870
|
+
| `no-brand-names` | `allowedSubstrings`, `brands` |
|
|
871
|
+
| `no-card-shaped-div` | — |
|
|
872
|
+
| `no-cross-feature-stores` | — |
|
|
873
|
+
| `no-d1-transaction` | — |
|
|
874
|
+
| `no-duplicate-jsx-patterns` | — |
|
|
875
|
+
| `no-hook-in-component-disguise` | — |
|
|
876
|
+
| `no-index-count-as-exact` | `estimateFields`, `labelKey` |
|
|
877
|
+
| `no-inert-hidden-jsx` | — |
|
|
878
|
+
| `no-inline-data-in-jsx` | — |
|
|
879
|
+
| `no-invalid-feature-folders` | — |
|
|
880
|
+
| `no-loader-side-effect-in-index-file` | `directories`, `resourceFactories` |
|
|
881
|
+
| `no-logic-in-component-files` | — |
|
|
882
|
+
| `no-mock-db-in-integration` | `dbModules`, `integrationDirs`, `mockNames` |
|
|
883
|
+
| `no-orm-outside-db` | — |
|
|
884
|
+
| `no-raw-html-atoms` | (a positional list), `scope` |
|
|
885
|
+
| `no-raw-sql-outside-allowed` | — |
|
|
886
|
+
| `no-react-namespace` | — |
|
|
887
|
+
| `no-renamed-html-props` | — |
|
|
888
|
+
| `no-render-prop-reader` | — |
|
|
889
|
+
| `no-trivial-wrapper-component` | — |
|
|
890
|
+
| `no-ts-in-bio-folders` | — |
|
|
891
|
+
| `no-type-definitions-in-components` | — |
|
|
892
|
+
| `no-unregistered-plugin-dir` | `manifests`, `registry`, `roots` |
|
|
893
|
+
| `no-void-port` | — |
|
|
894
|
+
| `organelle-dependency` | `sharedFeatures` |
|
|
895
|
+
| `organelle-single-source` | — |
|
|
896
|
+
| `plugin-route-namespaced` | `packages`, `scopes` |
|
|
897
|
+
| `queries-require-org-scope` | — |
|
|
898
|
+
| `queue-loop-is-the-library` | — |
|
|
899
|
+
| `route-no-inline-mutation` | `inlineMutationOk`, `mutationPrefixes`, `paths` |
|
|
900
|
+
| `router-schema-parity` | `ignore`, `mounts`, `routers`, `schemas`, `suffix` |
|
|
901
|
+
| `service-name-not-reserved` | `moduleFactories`, `reserved` |
|
|
902
|
+
| `ssot-no-inline-facts` | — |
|
|
903
|
+
| `ssot-no-process-env` | — |
|
|
904
|
+
| `step-declares-compensation-or-none` | `stepFactories` |
|
|
905
|
+
| `step-opens-its-own-cell` | `engine`, `hosts`, `scopeKey`, `within`, `wrapper` |
|
|
906
|
+
| `store-route-scopes-tenant-data` | — |
|
|
907
|
+
| `subpath-over-barrel` | `packages` |
|
|
908
|
+
| `subscriber-declares-id` | `paths` |
|
|
909
|
+
| `tables-declare-their-plane` | — |
|
|
910
|
+
| `tenant-tables-carry-org-id` | — |
|
|
911
|
+
| `time-through-the-door` | `door`, `exempt`, `libraries` |
|
|
912
|
+
| `tissue-must-compose` | — |
|
|
913
|
+
| `tissue-no-data-props` | — |
|
|
914
|
+
| `tissue-no-hooks` | — |
|
|
915
|
+
| `tissue-no-organelles` | — |
|
|
916
|
+
| `tissue-no-stores` | — |
|
|
917
|
+
| `todo-names-its-plan` | `id`, `markers` |
|
|
918
|
+
| `tokens-only-colors` | `allow`, `tokenPrefix` |
|
|
919
|
+
| `unique-query-step-name` | `queryStepCalls`, `workflowFactories` |
|
|
920
|
+
| `worker-handles-are-scoped` | `within`, `wrapper` |
|
|
921
|
+
| `zustand-v5-best-practices` | — |
|
|
922
|
+
|
|
923
|
+
<!-- END GENERATED -->
|
|
924
|
+
|
|
925
|
+
<!-- GENERATED: the floor each rule’s pattern belongs to -->
|
|
926
|
+
|
|
927
|
+
## The floor each rule’s pattern belongs to
|
|
928
|
+
|
|
929
|
+
A rule here lints a pattern one of the kit’s floors implements. While the floor is PENDING the
|
|
930
|
+
rule is a requirement with nothing to install behind it — a repo enabling it writes the pattern
|
|
931
|
+
itself, and the doctor says so on the `exercised` line.
|
|
932
|
+
|
|
933
|
+
| Rule | Floor | Shipped |
|
|
934
|
+
|---|---|---|
|
|
935
|
+
| `document-sagas-are-generic` | `document` | PENDING |
|
|
936
|
+
| `documents-share-one-table` | `document` | PENDING |
|
|
937
|
+
| `step-opens-its-own-cell` | `workflows` | yes |
|
|
938
|
+
|
|
939
|
+
<!-- END GENERATED FLOORS -->
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createStep } from '@corpus/workflows'
|
|
2
|
+
|
|
3
|
+
// BREACH: the default spelled out one level down. `limit: 0` inside this engine's budget object is
|
|
4
|
+
// the same silence as Medusa's `maxRetries: 0` — the first dropped connection reverts the write.
|
|
5
|
+
export const publish = createStep(
|
|
6
|
+
{ name: 'publish', retry: { backoff: 'exponential', limit: 0 } },
|
|
7
|
+
async (input, { store }) => store.createDocument(input),
|
|
8
|
+
async ({ id }, { store }) => store.deleteDocument(id),
|
|
9
|
+
)
|
package/dist/index.js
CHANGED
|
@@ -899,6 +899,7 @@ through \`perKindPrefixes\`, the tree that holds the flows through \`within\` (a
|
|
|
899
899
|
filename), and \`factories\` if the repo declares them under other names; enabled without
|
|
900
900
|
\`perKindPrefixes\` or \`within\` the rule refuses the run.`,
|
|
901
901
|
meta: {
|
|
902
|
+
floor: { name: "document", pending: true },
|
|
902
903
|
docs: {
|
|
903
904
|
description: "Document sagas are minted, not written: `documentSagas(entity, effects)` gives every entity that extends Document its create/save/setStatus/remove/send/convert/duplicate/share from one body. A saga or workflow named with a LITERAL for one entity is a fork; a name assembled from the entity is the mint and is unreadable here on purpose. `factories`, `perKindPrefixes` and `within` are options."
|
|
904
905
|
},
|
|
@@ -963,6 +964,7 @@ the dialect is a seam fact, not a reason for a kind to own storage. The forbidde
|
|
|
963
964
|
\`perKindTables\` option; enabled without it the rule refuses the run rather than passing every
|
|
964
965
|
schema silently.`,
|
|
965
966
|
meta: {
|
|
967
|
+
floor: { name: "document", pending: true },
|
|
966
968
|
docs: {
|
|
967
969
|
description: "Documents share ONE table: `documents` holds the head every kind derives and the body its kind declares. A table named for one kind of document is a second storage."
|
|
968
970
|
},
|
|
@@ -1842,6 +1844,7 @@ var writeNameOf = (node) => propertyNameOf(node.callee ?? {}) ?? "a service writ
|
|
|
1842
1844
|
// src/rules/mutating-step-declares-retry.ts
|
|
1843
1845
|
var RULE5 = "mutating-step-declares-retry";
|
|
1844
1846
|
var DEFAULT_RETRY_KEY = "maxRetries";
|
|
1847
|
+
var DEFAULT_RETRY_LIMIT_KEY = "limit";
|
|
1845
1848
|
var NO_RETRY2 = 0;
|
|
1846
1849
|
var mutatingStepDeclaresRetry = {
|
|
1847
1850
|
create(context) {
|
|
@@ -1852,6 +1855,11 @@ var mutatingStepDeclaresRetry = {
|
|
|
1852
1855
|
context.options?.[0]?.retryKey ?? DEFAULT_RETRY_KEY,
|
|
1853
1856
|
"retryKey"
|
|
1854
1857
|
);
|
|
1858
|
+
const retryLimitKey = requireOption(
|
|
1859
|
+
RULE5,
|
|
1860
|
+
context.options?.[0]?.retryLimitKey ?? DEFAULT_RETRY_LIMIT_KEY,
|
|
1861
|
+
"retryLimitKey"
|
|
1862
|
+
);
|
|
1855
1863
|
return {
|
|
1856
1864
|
CallExpression(node) {
|
|
1857
1865
|
if (!isStepCall(node, factories)) return;
|
|
@@ -1874,10 +1882,13 @@ var mutatingStepDeclaresRetry = {
|
|
|
1874
1882
|
context.report({ data: { key: retryKey, step }, messageId: "noRetry", node });
|
|
1875
1883
|
return;
|
|
1876
1884
|
}
|
|
1877
|
-
|
|
1878
|
-
|
|
1885
|
+
const nested = propertiesOf(budget);
|
|
1886
|
+
const declared = nested === null ? budget : valueOf(nested, retryLimitKey);
|
|
1887
|
+
if (declared === void 0) return;
|
|
1888
|
+
if (declared.type !== "Literal" || typeof declared.value !== "number") return;
|
|
1889
|
+
if (declared.value > NO_RETRY2) return;
|
|
1879
1890
|
context.report({
|
|
1880
|
-
data: { budget: String(
|
|
1891
|
+
data: { budget: String(declared.value), key: retryKey, step },
|
|
1881
1892
|
messageId: "zeroRetries",
|
|
1882
1893
|
node
|
|
1883
1894
|
});
|
|
@@ -1888,14 +1899,16 @@ var mutatingStepDeclaresRetry = {
|
|
|
1888
1899
|
\`createStep({ name: 'place-order', ...RETRY_DB }, invoke, compensate)\`. A retry budget defaults to
|
|
1889
1900
|
none, so a step named with a bare string cannot have one \u2014 take the object form and put the budget in
|
|
1890
1901
|
it, from the repo's shared retry constants rather than as a number at the call site. A budget the
|
|
1891
|
-
rule cannot evaluate (a constant, a spread
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
\`
|
|
1902
|
+
rule cannot evaluate (a constant, a spread) passes: it says nothing rather than pushing the number
|
|
1903
|
+
back inline. A budget written as an OBJECT is read one level down \u2014 \`retry: { limit: 0 }\` is the
|
|
1904
|
+
default spelled out and is reported, \`{ limit: 3 }\` is not, and an object naming no limit at all
|
|
1905
|
+
says nothing. Read-only and compute steps are exempt \u2014 there is nothing to revert. Configure
|
|
1906
|
+
\`retryKey\` to the key this engine writes the budget under (Medusa's \`maxRetries\`, the org engine's
|
|
1907
|
+
\`retry\`), \`retryLimitKey\` if the number inside that object is not \`limit\`, \`stepFactories\` if
|
|
1908
|
+
the repo wraps \`createStep\`, and \`mutationPrefixes\` if its writes are named otherwise.`,
|
|
1896
1909
|
meta: {
|
|
1897
1910
|
docs: {
|
|
1898
|
-
description: "A mutating step declares a retry class. A budget left unsaid defaults to zero retries, so a transient failure reverts real state a retry would have absorbed. Read-only steps are exempt; a non-literal budget passes. `retryKey`, `stepFactories` and `mutationPrefixes` are options."
|
|
1911
|
+
description: "A mutating step declares a retry class. A budget left unsaid defaults to zero retries, so a transient failure reverts real state a retry would have absorbed. Read-only steps are exempt; a non-literal budget passes, and an object budget is read one level down. `retryKey`, `retryLimitKey`, `stepFactories` and `mutationPrefixes` are options."
|
|
1899
1912
|
},
|
|
1900
1913
|
messages: {
|
|
1901
1914
|
noConfig: 'Step "{{step}}" writes but is named with a bare string, so it carries no retry class \u2014 and `{{key}}` defaults to 0, which means a transient failure reverts real state. Take the object form and give it a retry class from the shared constants.',
|
|
@@ -1908,6 +1921,7 @@ to revert. Configure \`retryKey\` to the key this engine writes the budget under
|
|
|
1908
1921
|
properties: {
|
|
1909
1922
|
mutationPrefixes: { items: { type: "string" }, type: "array" },
|
|
1910
1923
|
retryKey: { type: "string" },
|
|
1924
|
+
retryLimitKey: { type: "string" },
|
|
1911
1925
|
stepFactories: { items: { type: "string" }, type: "array" }
|
|
1912
1926
|
},
|
|
1913
1927
|
type: "object"
|
|
@@ -4991,6 +5005,7 @@ On \`sagaflow-js\` there is no such seam and the older shape stays reachable: se
|
|
|
4991
5005
|
Configure \`within\`; \`hosts\`, \`scopeKey\`, \`engine\` and \`wrapper\` have defaults or are off. Enabled
|
|
4992
5006
|
without \`within\` the rule refuses the run.`,
|
|
4993
5007
|
meta: {
|
|
5008
|
+
floor: { name: "workflows" },
|
|
4994
5009
|
docs: {
|
|
4995
5010
|
description: "A durable host declares the scope its steps run in \u2014 `scope.perStep` on the runtime it builds \u2014 because a durable run hibernates and retries, so a handle opened once per run is dead by the time a later step uses it. A repo on an engine with no such seam names its `engine` and `wrapper` instead, and a saga importing the engine's `step` around that wrapper is the finding. The guarded tree is required; enabled without it the rule refuses the run."
|
|
4996
5011
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geonosis/oxlint-plugin-biological-architecture",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
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
|
+
"RULES.md",
|
|
39
40
|
"corpus",
|
|
40
41
|
"dist"
|
|
41
42
|
],
|
|
@@ -43,7 +44,7 @@
|
|
|
43
44
|
"oxlint": ">=1.77"
|
|
44
45
|
},
|
|
45
46
|
"devDependencies": {
|
|
46
|
-
"@geonosis/lint-parity": "2.
|
|
47
|
+
"@geonosis/lint-parity": "2.2.0"
|
|
47
48
|
},
|
|
48
49
|
"engines": {
|
|
49
50
|
"node": ">=22"
|