@rdjperron/lint 0.6.0 → 0.8.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 +283 -122
- package/dist/{config-CD2iCM6L.mjs → config-BCudqv3_.mjs} +58 -2
- package/dist/config-BCudqv3_.mjs.map +1 -0
- package/dist/config.d.mts +38 -1
- package/dist/config.mjs +2 -2
- package/dist/index.mjs +691 -7
- package/dist/index.mjs.map +1 -1
- package/dist/repo.d.mts +16 -27
- package/dist/repo.mjs +28 -164
- package/dist/repo.mjs.map +1 -1
- package/dist/verify.mjs +122 -50
- package/dist/verify.mjs.map +1 -1
- package/fixtures/README.md +5 -0
- package/fixtures/convex/explicit-table-ids.fixture.ts +4 -0
- package/fixtures/convex-checked-constructors.fixture.ts +4 -0
- package/fixtures/convex-require-validators.fixture.ts +4 -0
- package/package.json +8 -3
- package/dist/config-CD2iCM6L.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
# @rdjperron/lint
|
|
2
2
|
|
|
3
|
-
One Oxlint JS plugin, `rdjperron`, with twenty rules against low-evidence code
|
|
4
|
-
project spreads into its own `vite.config.ts`, the check that
|
|
5
|
-
the repository checks that hold a repo's documents to their own
|
|
3
|
+
One Oxlint JS plugin, `rdjperron`, with twenty-two rules against low-evidence code and unchecked
|
|
4
|
+
Convex functions, the config data a project spreads into its own `vite.config.ts`, the check that
|
|
5
|
+
proves every rule still fires, and the repository checks that hold a repo's documents to their own
|
|
6
|
+
conventions.
|
|
6
7
|
|
|
7
8
|
| Entry | What it gives you |
|
|
8
9
|
| ------------------------ | ---------------------------------------------------------------------------------- |
|
|
9
|
-
| `@rdjperron/lint` | The plugin. One `jsPlugins` entry, one prefix, twenty rules.
|
|
10
|
+
| `@rdjperron/lint` | The plugin. One `jsPlugins` entry, one prefix, twenty-two rules. |
|
|
10
11
|
| `@rdjperron/lint/config` | `recommended`, the override builders, and the `fmt`, `ignore` and `staged` blocks. |
|
|
11
|
-
| `@rdjperron/lint/repo` | `checkRepo`,
|
|
12
|
-
| `rdjperron-lint-verify` | The command that proves every rule still fires. No config,
|
|
12
|
+
| `@rdjperron/lint/repo` | `checkRepo`, three checks on a repo's Markdown links and research names. |
|
|
13
|
+
| `rdjperron-lint-verify` | The command that proves every rule still fires. No config, one optional flag. |
|
|
13
14
|
|
|
14
15
|
Seventeen of the rules are a copy of [dmmulroy/anti-slop](https://github.com/dmmulroy/anti-slop),
|
|
15
16
|
owned and edited here (`src/anti-slop/VENDORED.md` in this repository records the commit). The
|
|
16
|
-
other
|
|
17
|
+
other five are written here.
|
|
17
18
|
|
|
18
19
|
Measured on vite-plus 0.3.0, 0.3.1, 0.3.2 and 0.3.3, which is oxlint 1.79.0 through 1.83.0
|
|
19
|
-
(2026-
|
|
20
|
+
(2026-10-12). All twenty-two rules fired on all four, and so did Convex's own plugin loaded through
|
|
21
|
+
`convex()`.
|
|
20
22
|
|
|
21
23
|
## Install
|
|
22
24
|
|
|
@@ -28,6 +30,13 @@ That is the whole installation. `@oxlint/plugins`, which the rules import `defin
|
|
|
28
30
|
types from, is an ordinary dependency here and arrives with the package; there is no version of it
|
|
29
31
|
for you to pin or to move.
|
|
30
32
|
|
|
33
|
+
`@convex-dev/eslint-plugin` is an optional peer, `^5.0.0`. Install it beside this package in a
|
|
34
|
+
project that calls `convex()`, which loads it:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install --save-dev @convex-dev/eslint-plugin
|
|
38
|
+
```
|
|
39
|
+
|
|
31
40
|
`vite-plus` is an optional peer, `>=0.3.0`, so a project on plain oxlint can still load the plugin.
|
|
32
41
|
The floor is `react()`, not the rules: oxlint 1.77.0, which vite-plus 0.2.9 declares, has no
|
|
33
42
|
`react/immutability`, `react/purity` or `react/refs`, and refuses to build a config that names them.
|
|
@@ -91,20 +100,22 @@ specifier inside an `extends` object is refused; a package name is what works.
|
|
|
91
100
|
|
|
92
101
|
### The config exports
|
|
93
102
|
|
|
94
|
-
| Export | What it is
|
|
95
|
-
| --------------------------- |
|
|
96
|
-
| `plugin` | `{ name: "rdjperron", specifier: "@rdjperron/lint" }`.
|
|
97
|
-
| `RULE_IDS`, `RuleId` | The twenty prefixed ids, as a readonly tuple and as a type.
|
|
98
|
-
| `recommended` | The config object for `lint.extends`: the plugin entry, the rules below, the built-in choices, the plugin set, the categories and `options.typeAware`.
|
|
99
|
-
| `react(files, options?)` | The hooks rules, the compiler's rules and the globals. `options.env` is the environment those files run in, `{ browser: true }` by default, so a React Native consumer names its own. `options.useEffectAllowedIn` names the one file allowed to call `useEffect`, puts its name in every finding, and switches both `no-use-effect` and `react/exhaustive-deps` off for it.
|
|
100
|
-
| `i18n(files, options?)` | `rdjperron/no-hardcoded-jsx-text` at error. `options.pattern` sets the word pattern.
|
|
101
|
-
| `expo(files, options?)` | The React Native facts: `static-public-env` at error, `typescript/no-require-imports` relaxed for asset extensions, `unicorn/no-array-sort` off. `options.assetExts` adds your `metro.config.js` extensions, `options.reactFiles` adds the two `react/*` rules an Expo app needs.
|
|
102
|
-
| `restrictedImports(…)` | One `no-restricted-imports` override per file set, expanded from one declaration. See below.
|
|
103
|
-
| `tests(files)` | `any`, `!`, `no-runtime-typeof` and the safety-comment rule off, with the reason on each.
|
|
104
|
-
| `
|
|
105
|
-
| `
|
|
106
|
-
| `
|
|
107
|
-
| `
|
|
103
|
+
| Export | What it is |
|
|
104
|
+
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
105
|
+
| `plugin` | `{ name: "rdjperron", specifier: "@rdjperron/lint" }`. |
|
|
106
|
+
| `RULE_IDS`, `RuleId` | The twenty-two prefixed ids, as a readonly tuple and as a type. |
|
|
107
|
+
| `recommended` | The config object for `lint.extends`: the plugin entry, the rules below, the built-in choices, the plugin set, the categories and `options.typeAware`. |
|
|
108
|
+
| `react(files, options?)` | The hooks rules, the compiler's rules and the globals. `options.env` is the environment those files run in, `{ browser: true }` by default, so a React Native consumer names its own. `options.useEffectAllowedIn` names the one file allowed to call `useEffect`, puts its name in every finding, and switches both `no-use-effect` and `react/exhaustive-deps` off for it. |
|
|
109
|
+
| `i18n(files, options?)` | `rdjperron/no-hardcoded-jsx-text` at error. `options.pattern` sets the word pattern. |
|
|
110
|
+
| `expo(files, options?)` | The React Native facts: `static-public-env` at error, `typescript/no-require-imports` relaxed for asset extensions, `unicorn/no-array-sort` off. `options.assetExts` adds your `metro.config.js` extensions, `options.reactFiles` adds the two `react/*` rules an Expo app needs. |
|
|
111
|
+
| `restrictedImports(…)` | One `no-restricted-imports` override per file set, expanded from one declaration. See below. |
|
|
112
|
+
| `tests(files)` | `any`, `!`, `no-runtime-typeof` and the safety-comment rule off, with the reason on each. |
|
|
113
|
+
| `convex(files, options?)` | Convex's own rules from `@convex-dev/eslint-plugin`, loaded by the override itself, and the two `rdjperron/convex-*` rules. `options.wrapper` names the module that builds the checked constructors and `options.wrapperImports` its alias spellings, `options.returns: false` stops asking for `returns`, `options.tests` names test code that may `.collect()` and `.filter()`, `options.hint` adds the project's reason to each checked-constructor finding. See below. |
|
|
114
|
+
| `convexPlugin` | `{ name: "@convex-dev", specifier: "@convex-dev/eslint-plugin" }`, the entry `convex()` puts in its override. |
|
|
115
|
+
| `decoders(files, options?)` | `no-runtime-typeof` and `no-unknown-parameters` at error everywhere, each hinting at the files in `files`, and `no-runtime-typeof` off inside them. `options.hint` replaces the generated sentence. |
|
|
116
|
+
| `fmt` | `printWidth` 100, double quotes, semicolons, two spaces. |
|
|
117
|
+
| `ignore` | Dependencies, build output, the AI agent directories and the Vitest JSON report. |
|
|
118
|
+
| `staged` | `vp check --fix` on JS and TS sources. |
|
|
108
119
|
|
|
109
120
|
`recommended` puts every rule at `error` except `no-hardcoded-jsx-text` and `static-public-env`,
|
|
110
121
|
which belong to a translated app and to an Expo app and are left to `i18n()` and `expo()`. The
|
|
@@ -163,14 +174,115 @@ scope and the entry:
|
|
|
163
174
|
The last two exist because both are a claim about the parent that is false, and a config that
|
|
164
175
|
states something false about its parent is one rename away from lifting a ban.
|
|
165
176
|
|
|
177
|
+
### `convex`, the backend's rules
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
...convex(["convex/**"], {
|
|
181
|
+
wrapper: "convex/lib/functions.ts",
|
|
182
|
+
tests: ["convex/**/*.test.ts", "convex/**/*.helpers.ts"],
|
|
183
|
+
}),
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
That returns two overrides. The first, on `files`, loads `@convex-dev/eslint-plugin` through its
|
|
187
|
+
own `jsPlugins` and sets eleven of its rules, eight at error and three off. The second, on
|
|
188
|
+
`tests`, switches `no-collect-in-query` and `no-filter-in-query` off, because a test reads a
|
|
189
|
+
table it filled itself.
|
|
190
|
+
|
|
191
|
+
| Rule | `convex()` | Why |
|
|
192
|
+
| ----------------------------------------------- | ---------- | ------------------------------------------------------------------------------------ |
|
|
193
|
+
| `@convex-dev/no-old-registered-function-syntax` | error | A bare handler has nowhere to put a validator. |
|
|
194
|
+
| `@convex-dev/explicit-table-ids` | error | `db.get(id)` without its table is the old form; the table goes first. |
|
|
195
|
+
| `@convex-dev/no-filter-in-query` | error | `.filter()` reads every row an index would have skipped. |
|
|
196
|
+
| `@convex-dev/no-top-of-hour-crons` | error | Minute 0 UTC is the busiest point on the clock. |
|
|
197
|
+
| `@convex-dev/no-schema-import-cycle` | error | A file the schema imports that reads the schema finds it undefined. |
|
|
198
|
+
| `@convex-dev/no-duplicate-indexes` | error | An index that prefixes another answers no new query and slows every write. |
|
|
199
|
+
| `@convex-dev/no-process-env` | error | The typed `env` from `_generated/server` knows which variables exist. |
|
|
200
|
+
| `@convex-dev/no-collect-in-query` | error | `.collect()` fails once a table outgrows the query limits. |
|
|
201
|
+
| `@convex-dev/require-args-validator` | off | `rdjperron/convex-require-validators` reports the same omissions, on more functions. |
|
|
202
|
+
| `@convex-dev/import-wrong-runtime` | off | It reads `use node` anywhere in a file's first 100 characters, comments included. |
|
|
203
|
+
| `@convex-dev/require-access-control` | off | It cannot see a function built by a checked constructor. |
|
|
204
|
+
|
|
205
|
+
oxlint gives a JS plugin no type information, so `explicit-table-ids` and `no-collect-in-query`
|
|
206
|
+
run on their syntax fallbacks. They match `x.db.get(id)` and `x.db.query(...)...collect()` by
|
|
207
|
+
shape, and report without the autofix the type checker would give them. The measurements behind
|
|
208
|
+
each severity are beside it in `src/config.ts`.
|
|
209
|
+
|
|
210
|
+
`rdjperron/convex-require-validators` is on in every case, asking for `args` and `returns`. With
|
|
211
|
+
`wrapper`, `rdjperron/convex-checked-constructors` is on too, and both rules know the wrapper.
|
|
212
|
+
|
|
213
|
+
Both rules recognise a Convex function by where its constructor comes from, not by its name. A
|
|
214
|
+
name counts when its binding in the file leads to `_generated/server`, to the generic constructors
|
|
215
|
+
in `convex/server`, or to the wrapper. A `query` imported from any other module is not Convex's,
|
|
216
|
+
and a name no binding in the file declares is taken by its spelling, the way Convex's own rules
|
|
217
|
+
take it.
|
|
218
|
+
|
|
219
|
+
The analysis stays inside one file and reads syntax and bindings. It follows a renamed import; a
|
|
220
|
+
namespace import read member by member, destructured, collected with `...rest` or held in a
|
|
221
|
+
`const`; an object literal, its own keys and its spreads, in the order they run, so a later key
|
|
222
|
+
replaces an earlier one; and `await import()` of a string or a template with no expression. A
|
|
223
|
+
value it cannot read this way is unknown, and nothing unknown is reported.
|
|
224
|
+
|
|
225
|
+
- Outside the wrapper, `convex-checked-constructors` reports `query`, `mutation` and `action` from
|
|
226
|
+
`_generated/server`, the generic ones from `convex/server`, and `getAuthUserId` and
|
|
227
|
+
`getAuthSessionId` from `@convex-dev/auth/server`, however they reach the file. Copying one of
|
|
228
|
+
those modules whole, by spread or by `...rest`, reports too unless every such name was taken out
|
|
229
|
+
beside it. It also reports reading `auth` off a context. A context is the first parameter of
|
|
230
|
+
the handler Convex will run, the last one the definition sets, given inline, by the name of a
|
|
231
|
+
function or a `const`, or in a spread the file can read. A parameter named `ctx` anywhere is one
|
|
232
|
+
too, since that is Convex's own name for it, and so is a `const` alias of either.
|
|
233
|
+
- `convex-require-validators` checks every value an export can hold: a `const`, a `let` with each
|
|
234
|
+
value it is assigned, and an alias of either. A validator set to `undefined`, `void 0` or a
|
|
235
|
+
`const` holding `undefined` declares nothing, as Convex reads it. A spread or definition it
|
|
236
|
+
cannot read, or a computed key, leaves that function unchecked rather than guessed at.
|
|
237
|
+
|
|
238
|
+
Where a value is not definitely known, the rules stay silent rather than guess. The analysis
|
|
239
|
+
stops, by choice rather than for want of types, at these forms, which are left unchecked:
|
|
240
|
+
|
|
241
|
+
- a context passed into a helper call, even one in the same file, or kept in a class field;
|
|
242
|
+
- a constructor returned by a factory, or read from another module, a barrel included;
|
|
243
|
+
- a computed key or import specifier;
|
|
244
|
+
- an export taken by destructuring, such as `export const { f } = { f: query({ … }) }` or a
|
|
245
|
+
property of a call's result;
|
|
246
|
+
- a `...rest` copy of an object literal, such as `const { ...t } = { q }`;
|
|
247
|
+
- a `const` copy of a `let` that is assigned more than once, since which value it copied depends
|
|
248
|
+
on when it ran.
|
|
249
|
+
|
|
250
|
+
The wrapper is one file. An absolute path pins it. A relative one is looked for from the working
|
|
251
|
+
directory upward, and the first directory that holds it is the root, so `convex/lib/functions.ts`
|
|
252
|
+
is the same file whether `vp lint` runs from the root or from `convex/`, and
|
|
253
|
+
`other/convex/lib/functions.ts` is not it. oxlint tells a rule the process's working directory,
|
|
254
|
+
not the config's, so a monorepo, or any project that may lint from elsewhere, passes the absolute
|
|
255
|
+
path: `fileURLToPath(new URL("./convex/lib/functions.ts", import.meta.url))`.
|
|
256
|
+
|
|
257
|
+
An import names the wrapper when a relative path resolves to that file: spelled with its own
|
|
258
|
+
extension, with none, or with the `.js` TypeScript uses for a `.ts` file. A directory's `/index` is
|
|
259
|
+
never collapsed, so `./lib/functions/index` is a different module from `lib/functions.ts`. Any
|
|
260
|
+
other spelling, such as `@/convex/lib/functions` from a tsconfig alias, names it only when
|
|
261
|
+
`wrapperImports` lists it exactly as written: the rules cannot read a bundler's or a tsconfig's
|
|
262
|
+
aliases, and guessing from the path would make the answer depend on where `vp lint` runs. With
|
|
263
|
+
an absolute `wrapper`, nothing about either rule depends on the working directory.
|
|
264
|
+
|
|
265
|
+
A later override replaces the entry of each rule it names, options included, and leaves the
|
|
266
|
+
others alone, so an exemption goes after `convex()`. A probe that reads `ctx.auth` on purpose:
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
...convex(["convex/**"], { wrapper: "convex/lib/functions.ts" }),
|
|
270
|
+
{ files: ["convex/probe.fixture.ts"], rules: { "rdjperron/convex-checked-constructors": "off" } },
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
That second override needs no `jsPlugins` of its own, and neither does one that changes an
|
|
274
|
+
`@convex-dev/*` rule: once any override loads a JS plugin, its rules resolve in every override
|
|
275
|
+
(measured on vite-plus 0.3.0 to 0.3.3, 2026-10-12).
|
|
276
|
+
|
|
166
277
|
## Prove the rules still fire
|
|
167
278
|
|
|
168
279
|
```bash
|
|
169
280
|
npx rdjperron-lint-verify
|
|
170
281
|
```
|
|
171
282
|
|
|
172
|
-
No configuration and
|
|
173
|
-
nothing" looks exactly like "no violations", so nothing else in a repo
|
|
283
|
+
No configuration, and one optional flag, `--require-convex`. A rule can go silently dead after a
|
|
284
|
+
linter upgrade, and "reports nothing" looks exactly like "no violations", so nothing else in a repo
|
|
285
|
+
notices. This asserts a
|
|
174
286
|
positive: every rule fired, each on the file named after it. It prints a table and exits non-zero on
|
|
175
287
|
the first rule that stayed quiet.
|
|
176
288
|
|
|
@@ -180,6 +292,14 @@ generated config that puts every rule at error, with your `node_modules` symlink
|
|
|
180
292
|
the whole directory afterwards whether the run passed or failed. Your own config, your ignore
|
|
181
293
|
patterns and your severities take no part.
|
|
182
294
|
|
|
295
|
+
Where `@convex-dev/eslint-plugin` is installed, the same run lints `fixtures/convex/` under the
|
|
296
|
+
overrides `convex()` returns and asserts that the Convex rule each file is named after fired. That
|
|
297
|
+
plugin loads through the override, not through anything you wrote, so this is what turns red when
|
|
298
|
+
the load stops working. Installed means its directory is in `node_modules`: a broken package still
|
|
299
|
+
runs the check, and fails it. A package your `package.json` declares but that is not installed
|
|
300
|
+
fails the run. With no plugin at all the command says it skipped that part, and
|
|
301
|
+
`--require-convex` turns that skip into a failure, which is what a Convex project's CI wants.
|
|
302
|
+
|
|
183
303
|
The linter it runs is the oxlint your local `vite-plus` depends on, so the run that matters is the
|
|
184
304
|
one right after you move `vite-plus`, and in CI.
|
|
185
305
|
|
|
@@ -188,25 +308,21 @@ dead.
|
|
|
188
308
|
|
|
189
309
|
## Repository checks
|
|
190
310
|
|
|
191
|
-
`@rdjperron/lint/repo` is for a repo that keeps
|
|
192
|
-
dated research in `_research/`. Prose can state those conventions and cannot enforce them.
|
|
193
|
-
reads the tree and returns what breaks them, so a test fails on a dead link or on a
|
|
194
|
-
|
|
311
|
+
`@rdjperron/lint/repo` is for a repo that keeps living documents at its root and under `docs/`, and
|
|
312
|
+
dated research in `_research/`. Prose can state those conventions and cannot enforce them.
|
|
313
|
+
`checkRepo` reads the tree and returns what breaks them, so a test fails on a dead link or on a
|
|
314
|
+
research entry with no date in its name.
|
|
195
315
|
|
|
196
316
|
It is plain Node and imports no test runner. You call it from a test of your own:
|
|
197
317
|
|
|
198
318
|
```ts
|
|
199
319
|
// lib/repo.test.ts
|
|
200
|
-
import { fileURLToPath } from "node:url";
|
|
320
|
+
import { fileURLToPath, URL } from "node:url";
|
|
201
321
|
|
|
202
322
|
import { CHECKS, checkRepo, HINTS, TITLES } from "@rdjperron/lint/repo";
|
|
203
323
|
import { describe, expect, it } from "vitest";
|
|
204
324
|
|
|
205
|
-
const faults = checkRepo({
|
|
206
|
-
root: fileURLToPath(new URL("..", import.meta.url)),
|
|
207
|
-
codeRoots: ["app", "components", "lib", "convex", "e2e"],
|
|
208
|
-
linked: ["e2e/README.md"],
|
|
209
|
-
});
|
|
325
|
+
const faults = checkRepo({ root: fileURLToPath(new URL("..", import.meta.url)) });
|
|
210
326
|
|
|
211
327
|
describe("the tree", () => {
|
|
212
328
|
for (const check of CHECKS) {
|
|
@@ -225,92 +341,73 @@ line that opens with a path relative to the root, with the line number when the
|
|
|
225
341
|
|
|
226
342
|
```
|
|
227
343
|
README.md:63 points at docs/not-there.md, which is not there
|
|
228
|
-
src/main.tsx:43 cites D16, which is retired
|
|
229
344
|
_research/notes.md is not named YYYYMMDD.HHMM.topic.md or YYYYMMDD.topic/
|
|
230
|
-
|
|
345
|
+
e2e/README.md, named by `linked`, is not there
|
|
231
346
|
```
|
|
232
347
|
|
|
233
348
|
`TITLES` holds what each check promises, for the test's name. `HINTS` holds one sentence per check
|
|
234
349
|
saying how to fix it, which the example passes to `expect` as the failure message. `CHECKS` is the
|
|
235
|
-
|
|
350
|
+
three names: `scans`, `links`, `research`.
|
|
236
351
|
|
|
237
|
-
`checkRepo` does not throw on a tree or a config it cannot read. A
|
|
238
|
-
|
|
239
|
-
|
|
352
|
+
`checkRepo` does not throw on a tree or a config it cannot read. A linked document that is a
|
|
353
|
+
directory, a symbolic link that leads nowhere and a root that does not exist are each a fault in
|
|
354
|
+
`scans`, and a link whose percent-escapes do not decode is a fault in `links`.
|
|
240
355
|
|
|
241
356
|
### What each check holds
|
|
242
357
|
|
|
243
|
-
| Check
|
|
244
|
-
|
|
|
245
|
-
| `links`
|
|
246
|
-
| `
|
|
247
|
-
| `
|
|
248
|
-
| `research` | Each entry of `_research/` is a file `YYYYMMDD.HHMM.topic.md` or a folder `YYYYMMDD.topic/` that holds a `README.md`. The topic is lowercase letters, digits and hyphens. |
|
|
249
|
-
| `scans` | Every path the config names is there and is the right kind, a file or a directory, defaults included. Every code root yields a file to scan. The register has a row. No symbolic link a walk meets leads nowhere. |
|
|
358
|
+
| Check | What it holds |
|
|
359
|
+
| ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
360
|
+
| `links` | Every relative Markdown link in the living documents points at a path that exists. So does every link in a research folder's `README.md` that stays inside the research directory. A URL with a scheme, a `//host` and a bare `#anchor` are skipped, and an anchor after a path is dropped before the path is tested. |
|
|
361
|
+
| `research` | Each entry of `_research/` is a file `YYYYMMDD.HHMM.topic.md` or a folder `YYYYMMDD.topic/` that holds a `README.md`. The topic is lowercase letters, digits and hyphens. |
|
|
362
|
+
| `scans` | Every path the config names is there and is the right kind, a file or a directory. A default that is there is the right kind too. No symbolic link at one of those paths, or that a walk meets, leads nowhere. |
|
|
250
363
|
|
|
251
|
-
`scans` exists because a check that reads nothing passes.
|
|
252
|
-
the config and
|
|
253
|
-
missing
|
|
364
|
+
`scans` exists because a check that reads nothing passes. Move `e2e/README.md` without touching
|
|
365
|
+
the config and its links would stop being read while the test stays green. With `scans` the
|
|
366
|
+
missing document is a fault.
|
|
254
367
|
|
|
255
|
-
`scans` holds paths
|
|
256
|
-
|
|
257
|
-
|
|
368
|
+
`scans` holds paths, not matches. A repo with no relative link yet is clean, so a repository can
|
|
369
|
+
turn the test on from its first commit, with its root alone for a config. A default that is absent
|
|
370
|
+
is not a fault: a repo may have no `BACKLOG.md`, no `docs` directory and no research yet. A path
|
|
371
|
+
the config names must be there, in `documents`, `docs` or `linked`. The research directory is the
|
|
372
|
+
one exception, and may be absent even when the config names it. Absent means nothing is at the
|
|
373
|
+
path: a symbolic link that leads nowhere is a fault, at a default path too.
|
|
258
374
|
|
|
259
375
|
The living documents are `README.md`, `AGENTS.md` and `BACKLOG.md` at the root, every `.md` file
|
|
260
|
-
under `docs/`, and the documents `linked` names.
|
|
261
|
-
|
|
262
|
-
scanned extension, and every file at the root with one. A lockfile is never scanned, meaning a
|
|
263
|
-
name that ends in `-lock.json`, `-lock.yaml` or the like.
|
|
376
|
+
under `docs/`, and the documents `linked` names. Every relative link in them is held, a link into
|
|
377
|
+
the research included.
|
|
264
378
|
|
|
265
379
|
The research is frozen once written, so the checks read little of it. The body of a research
|
|
266
|
-
entry is checked for
|
|
267
|
-
|
|
268
|
-
directory
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
When the register cannot be read, `scans` says so once, and `register` and `citations` stay empty
|
|
273
|
-
rather than failing every citation in the repo.
|
|
380
|
+
entry is not checked for links, since it names paths as they stood on its date. Only a research
|
|
381
|
+
folder's `README.md` has its links held, and only the links that stay inside the research
|
|
382
|
+
directory. A link out of it, to `../../docs/product.md` say, pointed at a document of its day, and
|
|
383
|
+
the living tree has moved on since, so it is not held. This goes for the research directory
|
|
384
|
+
wherever it is, under `docs/` included, and for a document inside it that `linked` names.
|
|
274
385
|
|
|
275
386
|
### The config
|
|
276
387
|
|
|
277
|
-
Every path is relative to the root, and `
|
|
388
|
+
Every path is relative to the root, and `docs`, `./docs` and `docs/` name the same one.
|
|
278
389
|
|
|
279
|
-
| Field
|
|
280
|
-
|
|
|
281
|
-
| `root`
|
|
282
|
-
| `
|
|
283
|
-
| `
|
|
284
|
-
| `
|
|
285
|
-
| `
|
|
286
|
-
| `
|
|
287
|
-
| `docs` | `docs` | The directory whose every `.md` file is a living document. |
|
|
288
|
-
| `linked` | none | More living documents, such as `e2e/README.md`. |
|
|
289
|
-
| `register` | `decisions.md` in the `docs` directory | The decision register. It follows `docs`: with `docs: "handbook"` it is `handbook/decisions.md`. |
|
|
290
|
-
| `research` | `_research` | The research directory. It may be absent. |
|
|
390
|
+
| Field | Default | What it is |
|
|
391
|
+
| ----------- | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
392
|
+
| `root` | required | The repository root. A relative one is resolved against the working directory. |
|
|
393
|
+
| `skip` | `node_modules` `_generated` `dist` | Names the walk of `docs` does not enter, wherever they sit. Yours are added to the default list. |
|
|
394
|
+
| `documents` | `README.md` `AGENTS.md` `BACKLOG.md` | The living documents at the root. A default that is absent is passed over. Yours replace the default list, and each one you name must be there. |
|
|
395
|
+
| `docs` | `docs` | The directory whose every `.md` file is a living document. Left at its default, it may be absent. |
|
|
396
|
+
| `linked` | none | More living documents, such as `e2e/README.md`. Each must be there. |
|
|
397
|
+
| `research` | `_research` | The research directory. It may be absent. |
|
|
291
398
|
|
|
292
|
-
`skip` matches a name at any depth, so `skip: ["fixtures"]` leaves out every `fixtures` directory
|
|
293
|
-
`
|
|
399
|
+
`skip` matches a name at any depth, so `skip: ["fixtures"]` leaves out every `fixtures` directory
|
|
400
|
+
under `docs`. It acts on what the walk finds under `docs`, and removes neither a document you name
|
|
401
|
+
in `documents` or `linked` nor an entry of the research directory. To stop holding a named
|
|
402
|
+
document, take it off its list.
|
|
294
403
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
```
|
|
298
|
-
|
|
299
|
-
Both act on what a walk finds under `docs`, under a code root and at the root. Neither removes a
|
|
300
|
-
document you name in `documents` or `linked`. To stop holding one of those, take it off its list.
|
|
301
|
-
|
|
302
|
-
A citation is any `D` followed by digits and standing as a word of its own, so recorded data full
|
|
303
|
-
of ids such as `D12` is the usual reason to leave a path out. Leave it out with `skip` or
|
|
304
|
-
`leftOut`, or keep its extension off the list, as with `csv`.
|
|
305
|
-
|
|
306
|
-
`DEFAULT_DOCUMENTS`, `DEFAULT_EXTENSIONS` and `DEFAULT_SKIP` are exported, for a config that adds
|
|
307
|
-
to a list the field replaces.
|
|
404
|
+
`DEFAULT_DOCUMENTS` and `DEFAULT_SKIP` are exported, for a config that adds to a list the field
|
|
405
|
+
replaces.
|
|
308
406
|
|
|
309
407
|
### What the checks do not do
|
|
310
408
|
|
|
311
|
-
They hold form, not truth. A link that resolves may point at the wrong document. A
|
|
312
|
-
|
|
313
|
-
rule. A research entry with a correct name may be empty.
|
|
409
|
+
They hold form, not truth. A link that resolves may point at the wrong document. A research entry
|
|
410
|
+
with a correct name may be empty.
|
|
314
411
|
|
|
315
412
|
They also leave these alone:
|
|
316
413
|
|
|
@@ -318,42 +415,43 @@ They also leave these alone:
|
|
|
318
415
|
- A link written as bare text, as `<path>`, inside an HTML tag, with a title in single quotes or
|
|
319
416
|
with parentheses in its path is not read as a link, or is read wrong.
|
|
320
417
|
- A link that opens with `/` is tested against the filesystem root, not the repository's.
|
|
321
|
-
-
|
|
322
|
-
-
|
|
323
|
-
- `#D12` and `-D16` count as citations, since the `D` still opens a word.
|
|
418
|
+
- A link out of the research is not tested at all, so one that was already dead on its day passes.
|
|
419
|
+
- The date in a research name is not checked to be a real date.
|
|
324
420
|
- Any file directly under the research directory that is not a dated note is a fault, a
|
|
325
421
|
`README.md` or a `.DS_Store` included.
|
|
326
422
|
- A linked directory is not entered, since it may lead back to one of its own parents.
|
|
327
423
|
|
|
328
|
-
Reading the
|
|
424
|
+
Reading the documents against the code is still a review's job.
|
|
329
425
|
|
|
330
426
|
## The rules
|
|
331
427
|
|
|
332
428
|
Each rule file opens with the reason the rule exists and the case where it is legitimate to
|
|
333
429
|
scope it off; the messages, the options and the cases are in the rule and its tests.
|
|
334
430
|
|
|
335
|
-
| Rule | What it rejects
|
|
336
|
-
| ----------------------------------------------------- |
|
|
337
|
-
| `rdjperron/
|
|
338
|
-
| `rdjperron/
|
|
339
|
-
| `rdjperron/no-
|
|
340
|
-
| `rdjperron/no-
|
|
341
|
-
| `rdjperron/no-
|
|
342
|
-
| `rdjperron/no-
|
|
343
|
-
| `rdjperron/no-
|
|
344
|
-
| `rdjperron/no-
|
|
345
|
-
| `rdjperron/no-
|
|
346
|
-
| `rdjperron/no-
|
|
347
|
-
| `rdjperron/no-
|
|
348
|
-
| `rdjperron/no-
|
|
349
|
-
| `rdjperron/no-
|
|
350
|
-
| `rdjperron/no-
|
|
351
|
-
| `rdjperron/no-unknown-
|
|
352
|
-
| `rdjperron/no-
|
|
353
|
-
| `rdjperron/no-
|
|
354
|
-
| `rdjperron/no-
|
|
355
|
-
| `rdjperron/
|
|
356
|
-
| `rdjperron/
|
|
431
|
+
| Rule | What it rejects |
|
|
432
|
+
| ----------------------------------------------------- | ------------------------------------------------------------------------------ |
|
|
433
|
+
| `rdjperron/convex-checked-constructors` | A raw public Convex constructor, or a read of the caller, outside the wrapper. |
|
|
434
|
+
| `rdjperron/convex-require-validators` | An exported Convex function without `args`, or without `returns`. |
|
|
435
|
+
| `rdjperron/no-array-filter-map` | `filter().map()`, two passes and an intermediate array. |
|
|
436
|
+
| `rdjperron/no-chained-type-assertions` | `x as unknown as T`, an assertion laundered through a second one. |
|
|
437
|
+
| `rdjperron/no-conditional-empty-object-spread` | `...(cond ? { a } : {})`, omission hidden behind an empty object. |
|
|
438
|
+
| `rdjperron/no-hardcoded-jsx-text` | Words written straight into JSX children. |
|
|
439
|
+
| `rdjperron/no-known-value-widening` | An annotation that throws away what inference already knew. |
|
|
440
|
+
| `rdjperron/no-module-mocking` | `vi.mock` and friends, a test of the mock rather than the module. |
|
|
441
|
+
| `rdjperron/no-object-parameters` | The bare `object` type on a function input. |
|
|
442
|
+
| `rdjperron/no-reduce-accumulator-copy` | Spreading the accumulator inside `reduce`, quadratic by construction. |
|
|
443
|
+
| `rdjperron/no-reflect-apply` | `Reflect.apply`, a call the types cannot follow. |
|
|
444
|
+
| `rdjperron/no-reflect-get` | `Reflect.get`, a property read the types cannot follow. |
|
|
445
|
+
| `rdjperron/no-runtime-typeof` | `typeof` as a substitute for parsing input at its boundary. |
|
|
446
|
+
| `rdjperron/no-shape-in-symbol-names` | The word "shape" in a symbol name, structure in place of a role. |
|
|
447
|
+
| `rdjperron/no-unknown-parameters` | `unknown` on a function input. |
|
|
448
|
+
| `rdjperron/no-unknown-returns` | `unknown` on a function output. |
|
|
449
|
+
| `rdjperron/no-unknown-type-aliases` | A type alias that resolves to `unknown`. |
|
|
450
|
+
| `rdjperron/no-unsafe-dictionary-type` | A dictionary whose values are `unknown`, `any` or `object`. |
|
|
451
|
+
| `rdjperron/no-use-effect` | Every `useEffect` and `useLayoutEffect` call or import. |
|
|
452
|
+
| `rdjperron/no-widen-then-assert` | Widening a value and asserting it back. |
|
|
453
|
+
| `rdjperron/require-safety-comment-for-type-assertion` | An assertion with no `SAFETY:` line naming the invariant. |
|
|
454
|
+
| `rdjperron/static-public-env` | A `process.env` read Expo's bundler does not replace at build time. |
|
|
357
455
|
|
|
358
456
|
### Options
|
|
359
457
|
|
|
@@ -368,6 +466,18 @@ scope it off; the messages, the options and the cases are in the rule and its te
|
|
|
368
466
|
- `rdjperron/no-unknown-parameters` takes `{ hint?: string }`, appended to every finding, so a project
|
|
369
467
|
can name the modules where the parse belongs. `decoders(files)` sets it.
|
|
370
468
|
- `rdjperron/static-public-env` has no options.
|
|
469
|
+
- `rdjperron/convex-checked-constructors` takes
|
|
470
|
+
`{ wrapper?: string, wrapperImports?: string[], hint?: string }`. `wrapper` exempts that one
|
|
471
|
+
file and names it in every finding. An absolute path pins it; a relative one is looked for from
|
|
472
|
+
the working directory upward. `wrapperImports` lists the alias spellings that import it. Without
|
|
473
|
+
`wrapper` every file is outside the wrapper, so the rule reports the wrapper's own imports too. `internalQuery`,
|
|
474
|
+
`internalMutation`, `internalAction` and `httpAction` stay allowed everywhere.
|
|
475
|
+
- `rdjperron/convex-require-validators` takes
|
|
476
|
+
`{ wrapper?: string, wrapperImports?: string[], returns?: boolean, hint?: string }`. `returns`
|
|
477
|
+
defaults to true. Convex's docs call a return validator optional, but its own `convex-expert`
|
|
478
|
+
skill asks for one on every function, and bidraft, atlas and rummy already declare one on every
|
|
479
|
+
function they deploy (2026-10-12). `httpAction` is left alone, since it takes a request.
|
|
480
|
+
- Both convex rules skip a file under `_generated/`, which `convex codegen` writes.
|
|
371
481
|
|
|
372
482
|
## Name the file that holds the fix
|
|
373
483
|
|
|
@@ -391,6 +501,57 @@ Land a new rule at `warn`, fix or scope what fires, then promote it. When a rule
|
|
|
391
501
|
code or add a narrowly scoped override with the measurement that justifies it. Never weaken the
|
|
392
502
|
rule, add a cast, or rename a symbol to dodge the check.
|
|
393
503
|
|
|
504
|
+
## Moving from 0.7
|
|
505
|
+
|
|
506
|
+
Nothing is removed and nothing changes for a project without Convex. 0.8.0 adds `convex()`, the two
|
|
507
|
+
`rdjperron/convex-*` rules and the optional peer `@convex-dev/eslint-plugin`.
|
|
508
|
+
|
|
509
|
+
A Convex project that bans the raw constructors by hand today deletes those bans when it adopts
|
|
510
|
+
`convex(files, { wrapper })`:
|
|
511
|
+
|
|
512
|
+
| You write by hand today | 0.8.0 gives you |
|
|
513
|
+
| --------------------------------------------------------------------------------------------------------- | --------------------------------------- |
|
|
514
|
+
| `query`, `mutation`, `action` from `_generated/server` in `no-restricted-imports`, lifted for the wrapper | `rdjperron/convex-checked-constructors` |
|
|
515
|
+
| `queryGeneric`, `mutationGeneric`, `actionGeneric` from `convex/server` | the same rule |
|
|
516
|
+
| `getAuthUserId`, `getAuthSessionId` from `@convex-dev/auth/server` | the same rule |
|
|
517
|
+
| `no-restricted-properties` on `ctx.auth` | the same rule |
|
|
518
|
+
|
|
519
|
+
The rule also sees what those bans missed: `const c = ctx; c.auth`, a handler whose context is
|
|
520
|
+
not named `ctx`, and a narrower override that restates `no-restricted-imports` and so drops the
|
|
521
|
+
Convex entries.
|
|
522
|
+
|
|
523
|
+
## Moving from 0.6
|
|
524
|
+
|
|
525
|
+
0.7.0 takes the decision register out of the repository checks. `checkRepo` no longer reads a
|
|
526
|
+
register, and no longer scans code for the `D<n>` numbers that cited its rows. A repo that keeps a
|
|
527
|
+
register stays on 0.6.0, which stays published.
|
|
528
|
+
|
|
529
|
+
| Gone | What it was for |
|
|
530
|
+
| ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
531
|
+
| The `register` check | It held `docs/decisions.md` to one well-formed line per row, each number given once. |
|
|
532
|
+
| The `citations` check | It held every `D<n>` in the scanned files to a live row of the register. |
|
|
533
|
+
| `codeRoots` | The directories scanned for citations. It was required, so delete it from your config. |
|
|
534
|
+
| `extensions` and `DEFAULT_EXTENSIONS` | The extensions the citation scan read. |
|
|
535
|
+
| `register` | The path of the register. |
|
|
536
|
+
| `leftOut` | Paths left out of the citation scan, for files whose contents read like `D<n>` numbers. Under `docs`, `skip` does the same by name. |
|
|
537
|
+
|
|
538
|
+
`CHECKS` is now `scans`, `links`, `research`, and `TITLES`, `HINTS` and the returned faults carry
|
|
539
|
+
those three keys. A test that loops over `CHECKS` needs no other edit. A test that filtered the
|
|
540
|
+
faults by hand to live without a register deletes the filter.
|
|
541
|
+
|
|
542
|
+
Three things change in what stays:
|
|
543
|
+
|
|
544
|
+
- `scans` no longer asks for a register or for code roots, and a default that is absent is not a
|
|
545
|
+
fault: a `docs` directory the config does not name, and any of `README.md`, `AGENTS.md` and
|
|
546
|
+
`BACKLOG.md` when the config leaves `documents` alone. A path the config names must still be
|
|
547
|
+
there.
|
|
548
|
+
- A research folder's `README.md` is held only for the links that stay inside the research
|
|
549
|
+
directory. A link from it to a living document that has since moved or gone no longer fails.
|
|
550
|
+
The same goes for a document inside the research directory that `linked` names: its links out
|
|
551
|
+
of the research are no longer held.
|
|
552
|
+
- `skip` acts on the walk of `docs` alone, since no code root and no file at the root is walked
|
|
553
|
+
any more.
|
|
554
|
+
|
|
394
555
|
## Moving from 0.5
|
|
395
556
|
|
|
396
557
|
Nothing is removed and nothing changes. 0.6.0 adds `@rdjperron/lint/repo`. A repo that carries the
|