@avi2dg/checks 0.10.0 → 0.12.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 +426 -89
- package/dist/feature-rules.js +242 -0
- package/package.json +25 -18
- package/presets/effect.language-service.json +8 -0
- package/presets/effect.oxlint.json +11 -0
- package/quality.schema.json +339 -0
- package/scripts/ci-wiring.ts +16 -43
- package/scripts/commit-identity.ts +4 -33
- package/scripts/feature-owners.ts +142 -0
- package/scripts/gates.ts +17 -15
- package/scripts/git.ts +70 -14
- package/scripts/lint.ts +7 -17
- package/scripts/quality-file.ts +298 -0
- package/scripts/quality.ts +180 -0
- package/scripts/size-budget.ts +158 -0
package/README.md
CHANGED
|
@@ -6,9 +6,12 @@ below over a range it resolves itself, the `checks-test` entry point
|
|
|
6
6
|
that runs the suite and refuses an undeclared skip, the `checks-flake`
|
|
7
7
|
run that records the seeds a failing test fails with, the oxlint base
|
|
8
8
|
config, the tsconfig fragment with the Effect language-service block,
|
|
9
|
-
the
|
|
10
|
-
|
|
9
|
+
the `quality.json` schema with the generator that turns its Effect paths
|
|
10
|
+
into oxlint and tsconfig fragments, the shared commitlint config, the
|
|
11
|
+
shared dependency-cruiser base with the feature-owner rules `quality.json`
|
|
12
|
+
compiles into it, the test-layout check with its bunfig preset, the commit-identity check, the
|
|
11
13
|
comment gate with its backtest, the oxlint suppressions ratchet, the
|
|
14
|
+
size budget, the feature-owner change signal and proof check, the
|
|
12
15
|
Stryker mutation-testing preset with its no-regression comparator, the
|
|
13
16
|
CI-wiring check, and the Effect error-channel plugin compiled to
|
|
14
17
|
JavaScript.
|
|
@@ -46,6 +49,17 @@ node_modules/
|
|
|
46
49
|
}
|
|
47
50
|
```
|
|
48
51
|
|
|
52
|
+
`quality.json` at the repository root declares what the repository
|
|
53
|
+
opts into, starting with the commands its CI runs; see "Quality file"
|
|
54
|
+
below:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"$schema": "./node_modules/@avi2dg/checks/quality.schema.json",
|
|
59
|
+
"gates": { "ci": ["bun run lint", "bun run typecheck", "bun run test"] }
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
49
63
|
`bunfig.toml` is a copy of the shipped preset:
|
|
50
64
|
|
|
51
65
|
```sh
|
|
@@ -92,12 +106,164 @@ export default {
|
|
|
92
106
|
The registry version is pinned by the consumer's lockfile; bump
|
|
93
107
|
`@avi2dg/checks` to adopt a new release.
|
|
94
108
|
|
|
109
|
+
## Quality file
|
|
110
|
+
|
|
111
|
+
`quality.json` at the repository root says what the repository has
|
|
112
|
+
opted into. The kit's bins find it at the git root and read it there:
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"$schema": "./node_modules/@avi2dg/checks/quality.schema.json",
|
|
117
|
+
"defaultBranch": "main",
|
|
118
|
+
"gates": {
|
|
119
|
+
"ci": ["bun run lint", "bun run typecheck", "bun run test"],
|
|
120
|
+
"scheduled": ["bunx checks-flake --runs 10 --report flake-report.json"]
|
|
121
|
+
},
|
|
122
|
+
"commitIdentity": { "authors": [{ "name": "avi2d", "email": "avi2dg@gmail.com" }] },
|
|
123
|
+
"sources": {
|
|
124
|
+
"production": ["src/**/*.ts"],
|
|
125
|
+
"effect": { "paths": ["src/**/*.ts"], "exempt": ["src/host/*.ts"] }
|
|
126
|
+
},
|
|
127
|
+
"size": { "fileLines": 400, "functionLines": 100, "applies": "changed" },
|
|
128
|
+
"features": [
|
|
129
|
+
{
|
|
130
|
+
"name": "billing",
|
|
131
|
+
"root": "src/billing",
|
|
132
|
+
"entries": ["src/billing/index.ts"],
|
|
133
|
+
"allowFrom": ["src/main.ts"],
|
|
134
|
+
"proof": "tests/e2e/billing.test.ts"
|
|
135
|
+
}
|
|
136
|
+
],
|
|
137
|
+
"changeSignal": "advisory",
|
|
138
|
+
"agentRules": { "on": [], "off": [] }
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
| Key | Read by | Holds |
|
|
143
|
+
| --- | --- | --- |
|
|
144
|
+
| `defaultBranch` | `checks-lint`, `checks-ci-wiring` | the branch pull requests merge into, `main` when absent |
|
|
145
|
+
| `gates.ci` | `checks-ci-wiring` | the commands CI runs on every pull request; see "CI wiring" |
|
|
146
|
+
| `gates.scheduled` | `checks-ci-wiring` | the commands a schedule runs |
|
|
147
|
+
| `gates.lint` | `checks-lint`, `checks-ci-wiring` | the gates `checks-lint` runs when not all apply; see "Gate selection" |
|
|
148
|
+
| `commitIdentity.authors` | `checks-commit-identity` | the identities allowed to author and commit; see "Commit identity" |
|
|
149
|
+
| `sources.effect` | `checks-quality` | the paths held to the Effect rules, and the files under them that are not; see "Effect rules" |
|
|
150
|
+
| `sources.production` | `checks-size-budget`, `checks-quality` | the source the repository ships; see "Size budget" |
|
|
151
|
+
| `size` | `checks-size-budget` | the line budget, and which production files it holds; see "Size budget" |
|
|
152
|
+
| `features` | `featureRules`, `checks-feature-owners` | each feature's root, entries, exempt importers and proof; see "Feature owners" |
|
|
153
|
+
| `changeSignal` | `checks-feature-owners` | `advisory` to list the feature owners a change touches; see "Feature owners" |
|
|
154
|
+
| `agentRules.on`, `agentRules.off` | agent Rule selection, not the kit | catalogued Rules switched on or off for this repository |
|
|
155
|
+
|
|
156
|
+
Every key is optional. The bins decode the file with one Effect
|
|
157
|
+
`Schema`, and the package ships `quality.schema.json` emitted from that
|
|
158
|
+
schema, so the `$schema` line gives an editor the verdict the bins
|
|
159
|
+
reach, save what no JSON Schema can express across two values, which
|
|
160
|
+
the bins refuse: a Rule switched both on and off, a feature entry
|
|
161
|
+
outside its root, and two features with one name or sharing a root. A key the schema does
|
|
162
|
+
not name is refused, not ignored, so a misspelt `sources` cannot switch
|
|
163
|
+
the Effect rules off unnoticed. A glob in `sources`
|
|
164
|
+
starts at the repository root, names a directory first, uses `*`
|
|
165
|
+
only within a segment and `**` only as a whole one, and ends in a file
|
|
166
|
+
name with an extension, which are the globs oxlint, the language
|
|
167
|
+
service and git all read alike. oxlint matches `*.ts` at any depth
|
|
168
|
+
where the other two match it at the root alone, so the schema refuses
|
|
169
|
+
it; `**/*.ts` means every depth to all three. The language service
|
|
170
|
+
matches nothing for `src/**` and oxlint nothing for `src/lib`, so the
|
|
171
|
+
schema refuses both; `src/**/*.ts` and `src/lib/*.ts` say it to all
|
|
172
|
+
three.
|
|
173
|
+
|
|
174
|
+
Until a later minor release, a repository with no `quality.json` still
|
|
175
|
+
has `ciWiring` and `commitIdentity` read from `package.json`, with a
|
|
176
|
+
notice on each read. One with both exits 2 until `package.json` drops
|
|
177
|
+
them. The keys map one for one:
|
|
178
|
+
|
|
179
|
+
| `package.json` | `quality.json` |
|
|
180
|
+
| --- | --- |
|
|
181
|
+
| `ciWiring.gates` | `gates.ci` |
|
|
182
|
+
| `ciWiring.scheduled` | `gates.scheduled` |
|
|
183
|
+
| `ciWiring.lintGates` | `gates.lint` |
|
|
184
|
+
| `ciWiring.defaultBranch` | `defaultBranch` |
|
|
185
|
+
| `commitIdentity` | `commitIdentity` |
|
|
186
|
+
|
|
187
|
+
### Generated fragments
|
|
188
|
+
|
|
189
|
+
oxlint and tsc read their own JSON and nothing else, so
|
|
190
|
+
`checks-quality generate` writes what `sources.effect` declares into two
|
|
191
|
+
fragments at the repository root, and the hand-written configs extend
|
|
192
|
+
them. Both fragments are committed:
|
|
193
|
+
|
|
194
|
+
```sh
|
|
195
|
+
checks-quality generate
|
|
196
|
+
checks-quality --check
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`.oxlintrc.json`:
|
|
200
|
+
|
|
201
|
+
```json
|
|
202
|
+
{
|
|
203
|
+
"extends": ["./node_modules/@avi2dg/checks/oxlintrc.json", "./oxlintrc.quality.json"],
|
|
204
|
+
"plugins": ["typescript", "oxc", "eslint", "import"]
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
`tsconfig.json`:
|
|
209
|
+
|
|
210
|
+
```json
|
|
211
|
+
{
|
|
212
|
+
"extends": ["@avi2dg/checks/tsconfig.effect.json", "./tsconfig.quality.json"]
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
`oxlintrc.quality.json` holds one override: the declared paths as
|
|
217
|
+
`files`, the exempt ones as `excludeFiles`, and the kit's Effect rule
|
|
218
|
+
block, `presets/effect.oxlint.json`. `tsconfig.quality.json` holds the
|
|
219
|
+
language-service override: the same paths as `include`, the exempt ones
|
|
220
|
+
as `exclude`, and `presets/effect.language-service.json`. A kit release
|
|
221
|
+
that changes a preset reaches the repository through its next
|
|
222
|
+
`generate`. A rule only this repository needs stays in its own
|
|
223
|
+
`.oxlintrc.json`, whose overrides come after the fragment's and so win.
|
|
224
|
+
|
|
225
|
+
`checks-lint` runs `checks-quality --check`, which exits 1 when:
|
|
226
|
+
|
|
227
|
+
- a fragment is missing, or differs from what `generate` would write
|
|
228
|
+
from `quality.json` and the installed kit's presets;
|
|
229
|
+
- a fragment is left over once `quality.json` stops declaring
|
|
230
|
+
`sources.effect`;
|
|
231
|
+
- `.oxlintrc.json` or `tsconfig.json` does not list its fragment in
|
|
232
|
+
`extends`, so the tool never reads it;
|
|
233
|
+
- a `sources.effect.paths` glob, or a `sources.production` glob while
|
|
234
|
+
`size` is declared, matches no tracked or untracked file, so it holds
|
|
235
|
+
nothing.
|
|
236
|
+
|
|
237
|
+
It exits 2 when `quality.json` does not decode. `generate` writes the
|
|
238
|
+
fragments, removes a left-over one, then runs the same check.
|
|
239
|
+
|
|
240
|
+
```
|
|
241
|
+
checks-quality: 2 problem(s) with what quality.json declares:
|
|
242
|
+
oxlintrc.quality.json is stale against quality.json and the kit's presets; run checks-quality generate
|
|
243
|
+
tsconfig.json does not extend ./tsconfig.quality.json, so the language service never reads it
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Two details of the fragments are easy to get wrong, so the kit's tests
|
|
247
|
+
pin both:
|
|
248
|
+
|
|
249
|
+
- A fragment sits at the repository root. oxlint resolves an
|
|
250
|
+
override's `files`, and the language service an override's `include`,
|
|
251
|
+
against the directory of the config holding it. From `.quality/` the
|
|
252
|
+
language service reports no error at all on an `async function`
|
|
253
|
+
planted under a declared path.
|
|
254
|
+
- The oxlint fragment always sets `plugins`, to the kit's. A config in
|
|
255
|
+
`extends` that sets none brings in oxlint's default plugins, whose
|
|
256
|
+
category rules then fire across the whole tree. The override names
|
|
257
|
+
the kit's plugins beside the preset's `node`, `promise` and `unicorn`,
|
|
258
|
+
because one that leaves any of the kit's out turns on the category
|
|
259
|
+
rules of the plugins it adds under every declared path.
|
|
260
|
+
|
|
95
261
|
## Lint entry point
|
|
96
262
|
|
|
97
263
|
`checks-lint` runs each of the kit's lint gates in turn and names every
|
|
98
264
|
one that fails, rather than stopping at the first. A repository whose
|
|
99
265
|
tracked files give a gate nothing to check can leave it out through
|
|
100
|
-
`
|
|
266
|
+
`gates.lint`; see "Gate selection" under "CI wiring".
|
|
101
267
|
|
|
102
268
|
| Gate | Reads |
|
|
103
269
|
| --- | --- |
|
|
@@ -107,6 +273,9 @@ tracked files give a gate nothing to check can leave it out through
|
|
|
107
273
|
| `checks-comment-gate` | the range |
|
|
108
274
|
| `checks-suppressions-ratchet` | the range |
|
|
109
275
|
| `checks-ci-wiring` | the working tree |
|
|
276
|
+
| `checks-quality` | the working tree |
|
|
277
|
+
| `checks-size-budget` | the range |
|
|
278
|
+
| `checks-feature-owners` | the range |
|
|
110
279
|
|
|
111
280
|
```sh
|
|
112
281
|
checks-lint
|
|
@@ -117,8 +286,8 @@ It resolves the range once and hands the same one to every range gate.
|
|
|
117
286
|
Locally, and on any event other than a pull request, the range ends at
|
|
118
287
|
`HEAD` and starts where `HEAD` branched from the origin default branch:
|
|
119
288
|
`origin/HEAD`, or when `origin/HEAD` is not set, as in an
|
|
120
|
-
`actions/checkout` clone, `origin/<
|
|
121
|
-
repository's `
|
|
289
|
+
`actions/checkout` clone, `origin/<defaultBranch>` from the
|
|
290
|
+
repository's `quality.json` (see "Quality file"), and `origin/main` when
|
|
122
291
|
that is not declared. In a GitHub Actions pull request, where
|
|
123
292
|
`GITHUB_EVENT_NAME` is `pull_request`, it ends
|
|
124
293
|
at the event's head sha and starts where that branched from
|
|
@@ -159,12 +328,12 @@ gate's own report, then its verdict:
|
|
|
159
328
|
```
|
|
160
329
|
checks-lint: range 2504acf098d120e73a8ece3c96f22b934f35c6a8..10ba7d8935b73ed72624120a1542e51bd21ca7c7 from HEAD against origin/main
|
|
161
330
|
...
|
|
162
|
-
checks-lint: 3 of
|
|
331
|
+
checks-lint: 3 of 9 gate(s) failed: checks-commit-identity, checks-comment-gate, checks-suppressions-ratchet
|
|
163
332
|
```
|
|
164
333
|
|
|
165
334
|
It exits 1 when any gate found a violation, and 2 when the range or the
|
|
166
335
|
selection does not resolve, or no failing gate could decide. ci-wiring
|
|
167
|
-
always runs, so a repository on `checks-lint` declares `
|
|
336
|
+
always runs, so a repository on `checks-lint` declares `gates.ci`
|
|
168
337
|
(see "CI wiring"), and it holds the test layout unless its selection
|
|
169
338
|
leaves out `checks-test-layout`.
|
|
170
339
|
|
|
@@ -205,69 +374,43 @@ Each refusal says what to write instead: a `Schema.TaggedError` failed
|
|
|
205
374
|
through `Effect.fail`, a throwing call wrapped in `Effect.try` or
|
|
206
375
|
`Effect.tryPromise`, and recovery by tag with `Effect.catchTag`.
|
|
207
376
|
|
|
208
|
-
A repository turns them on for the paths it writes in Effect
|
|
209
|
-
`
|
|
377
|
+
A repository turns them on for the paths it writes in Effect by
|
|
378
|
+
declaring those paths in `quality.json` and extending the generated
|
|
379
|
+
fragments; see "Generated fragments" under "Quality file":
|
|
210
380
|
|
|
211
381
|
```json
|
|
212
|
-
{
|
|
213
|
-
"
|
|
214
|
-
"plugins": ["typescript", "oxc", "eslint", "import"],
|
|
215
|
-
"overrides": [
|
|
216
|
-
{
|
|
217
|
-
"files": ["src/**"],
|
|
218
|
-
"rules": {
|
|
219
|
-
"effect-channel/no-throw": "error",
|
|
220
|
-
"effect-channel/no-try-catch": "error"
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
]
|
|
382
|
+
"sources": {
|
|
383
|
+
"effect": { "paths": ["src/**/*.ts"], "exempt": ["src/host/*.ts"] }
|
|
224
384
|
}
|
|
225
385
|
```
|
|
226
386
|
|
|
387
|
+
The fragment's override carries the kit's Effect rule block,
|
|
388
|
+
`presets/effect.oxlint.json`: the two rules above, plus `node/no-sync`,
|
|
389
|
+
`oxc/no-async-await`, `promise/avoid-new` and `unicorn/no-process-exit`.
|
|
390
|
+
Files under `exempt` answer to none of them.
|
|
391
|
+
|
|
227
392
|
oxlint resolves `files` against the directory of the config that holds
|
|
228
393
|
the override, so a config passed with `-c` from outside the repository
|
|
229
394
|
matches nothing and reports nothing.
|
|
230
395
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
a
|
|
236
|
-
|
|
237
|
-
baseline, `oxlint --suppress-all`,
|
|
238
|
-
|
|
239
|
-
|
|
396
|
+
`unicorn/no-process-exit` passes over any file that opens with a
|
|
397
|
+
shebang, so a repository whose bins open with one bans `process.exit`
|
|
398
|
+
itself with `no-restricted-properties` in its own `.oxlintrc.json`, as
|
|
399
|
+
this one does. The preset leaves that rule out because
|
|
400
|
+
a repository's own `no-restricted-properties` list for the same files
|
|
401
|
+
would replace it, or be replaced by it. Sites standing when the
|
|
402
|
+
declaration lands go in oxlint's own baseline, `oxlint --suppress-all`,
|
|
403
|
+
so their count can only fall. This repository's own `.oxlintrc.json`
|
|
404
|
+
also lifts `effect-channel/no-throw` from `scripts/comment-matchers.ts`,
|
|
405
|
+
the one file under its Effect path that a host loads without
|
|
406
|
+
`node_modules`.
|
|
240
407
|
|
|
241
408
|
The language service holds the same paths to Effect-native IO through
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
{
|
|
248
|
-
"extends": "@avi2dg/checks/tsconfig.effect.json",
|
|
249
|
-
"compilerOptions": {
|
|
250
|
-
"plugins": [
|
|
251
|
-
{
|
|
252
|
-
"name": "@effect/language-service",
|
|
253
|
-
"overrides": [
|
|
254
|
-
{
|
|
255
|
-
"include": ["src/**/*.ts"],
|
|
256
|
-
"options": {
|
|
257
|
-
"diagnosticSeverity": {
|
|
258
|
-
"nodeBuiltinImport": "error",
|
|
259
|
-
"asyncFunction": "error",
|
|
260
|
-
"newPromise": "error",
|
|
261
|
-
"extendsNativeError": "error"
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
]
|
|
266
|
-
}
|
|
267
|
-
]
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
```
|
|
409
|
+
the tsconfig fragment's override, whose severities are
|
|
410
|
+
`presets/effect.language-service.json`: `nodeBuiltinImport`,
|
|
411
|
+
`asyncFunction`, `newPromise` and `extendsNativeError`, all errors.
|
|
412
|
+
effect-tsgo keeps the severities `tsconfig.effect.json` sets when a later
|
|
413
|
+
config in `extends` restates the plugin with only its overrides.
|
|
271
414
|
|
|
272
415
|
## Test layout
|
|
273
416
|
|
|
@@ -412,7 +555,7 @@ jobs:
|
|
|
412
555
|
path: flake-report.json
|
|
413
556
|
```
|
|
414
557
|
|
|
415
|
-
and declares the step in `
|
|
558
|
+
and declares the step in `gates.scheduled`, so `checks-ci-wiring`
|
|
416
559
|
fails once the schedule stops running it; see "CI wiring".
|
|
417
560
|
|
|
418
561
|
## Dependency rules
|
|
@@ -443,7 +586,9 @@ boundary you own. A rule that restates a base name overrides it field
|
|
|
443
586
|
by field, which is how an entry point stops being an orphan:
|
|
444
587
|
redeclare `no-orphans` with your entry added to its `pathNot`.
|
|
445
588
|
This repo's own `.dependency-cruiser.cjs` does that for the plugin
|
|
446
|
-
entry.
|
|
589
|
+
entry. A repository that declares feature owners spreads the rules
|
|
590
|
+
`quality.json` compiles to into the same `forbidden`; see "Feature
|
|
591
|
+
owners".
|
|
447
592
|
|
|
448
593
|
`package.json` gains the script:
|
|
449
594
|
|
|
@@ -520,7 +665,7 @@ body are left alone. `GitHub <noreply@github.com>` is allowed as
|
|
|
520
665
|
committer only, since that is who writes a squash merge.
|
|
521
666
|
|
|
522
667
|
The allowlist defaults to `avi2d <avi2dg@gmail.com>`. A repo with other
|
|
523
|
-
owners restates it in `
|
|
668
|
+
owners restates it in `quality.json`:
|
|
524
669
|
|
|
525
670
|
```json
|
|
526
671
|
"commitIdentity": {
|
|
@@ -594,6 +739,160 @@ suppressions-ratchet: 2 count(s) in oxlint-suppressions.json rose or appeared; f
|
|
|
594
739
|
|
|
595
740
|
`checks-lint` runs it over each pull request's range; see "Lint entry point".
|
|
596
741
|
|
|
742
|
+
## Size budget
|
|
743
|
+
|
|
744
|
+
`checks-size-budget` holds production files to the line budget
|
|
745
|
+
`quality.json` declares, and lists every other file over it without
|
|
746
|
+
failing:
|
|
747
|
+
|
|
748
|
+
```json
|
|
749
|
+
"sources": { "production": ["src/**/*.ts"] },
|
|
750
|
+
"size": { "fileLines": 400, "functionLines": 100, "applies": "changed" }
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
```sh
|
|
754
|
+
checks-size-budget <base-ref> <head-ref>
|
|
755
|
+
checks-size-budget <ref>
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
It runs oxlint with a configuration of two rules and nothing else:
|
|
759
|
+
`max-lines` at `fileLines` and `max-lines-per-function` at
|
|
760
|
+
`functionLines`, both counting blank and comment lines. With `applies`
|
|
761
|
+
set to `changed` it holds the files under `sources.production` that the
|
|
762
|
+
range adds or changes, a rename that edits the file included. With
|
|
763
|
+
`all` it holds every file under `sources.production`. A file the range
|
|
764
|
+
deletes or only renames is not held. Every other tracked `.ts` or
|
|
765
|
+
`.tsx` file over the budget, tests and unchanged production files alike,
|
|
766
|
+
is listed as advisory and never fails the gate; `.d.ts` files are not
|
|
767
|
+
measured.
|
|
768
|
+
|
|
769
|
+
It reads each file from the head commit rather than the working tree,
|
|
770
|
+
so an uncommitted edit neither fails nor passes a range, and a pull
|
|
771
|
+
request's merge checkout measures what the pull request holds. With two
|
|
772
|
+
arguments the range starts where the head branched from the base, at
|
|
773
|
+
their merge-base. With one it is that commit against its parent, or
|
|
774
|
+
against the empty tree for a repository's first commit. oxlint must be
|
|
775
|
+
on `PATH`, as it is under a package script.
|
|
776
|
+
|
|
777
|
+
```
|
|
778
|
+
size-budget: 1 overrun(s) of 400 lines per file and 100 per function in the production files the range adds or changes:
|
|
779
|
+
src/billing/ledger.ts:12: The function `settle` has too many lines (131). Maximum allowed is 100.
|
|
780
|
+
size-budget: advisory, 1 overrun(s) where the budget does not hold yet:
|
|
781
|
+
tests/e2e/billing.test.ts: File has too many lines (512).
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
It exits 1 on an overrun in a file it holds, and 2 when `quality.json`
|
|
785
|
+
does not decode, a ref does not resolve or oxlint cannot run. A
|
|
786
|
+
repository that declares no `size` passes. `quality.json` refuses a
|
|
787
|
+
`size` without `sources.production`, which would hold nothing, and
|
|
788
|
+
with `size` declared `checks-quality` refuses a `sources.production`
|
|
789
|
+
glob that matches no file. Moving `applies` from `changed` to `all`
|
|
790
|
+
tightens the budget to every production file, once the advisory list
|
|
791
|
+
names none.
|
|
792
|
+
|
|
793
|
+
`checks-lint` runs it over each pull request's range; see "Lint entry point".
|
|
794
|
+
|
|
795
|
+
## Feature owners
|
|
796
|
+
|
|
797
|
+
A repository opts a feature in by declaring, in `quality.json`, the
|
|
798
|
+
directory it owns, the files code outside it imports it through, the
|
|
799
|
+
files that may reach past those, and the end-to-end test that proves it
|
|
800
|
+
runs:
|
|
801
|
+
|
|
802
|
+
```json
|
|
803
|
+
"features": [
|
|
804
|
+
{
|
|
805
|
+
"name": "billing",
|
|
806
|
+
"root": "src/billing",
|
|
807
|
+
"entries": ["src/billing/index.ts"],
|
|
808
|
+
"allowFrom": ["src/main.ts", "src/cli/*.ts"],
|
|
809
|
+
"proof": "tests/e2e/billing.test.ts"
|
|
810
|
+
}
|
|
811
|
+
],
|
|
812
|
+
"changeSignal": "advisory"
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
`root` is a directory and `entries` are files under it, both without
|
|
816
|
+
globs. `allowFrom` holds globs of the same shape as `sources`. `proof`
|
|
817
|
+
is a `.test.ts` or `.test.tsx` file under `tests/e2e/`. Nothing moves:
|
|
818
|
+
a root is wherever the feature already lives. `quality.json` refuses an
|
|
819
|
+
entry outside its root, a name used twice, and two features sharing a
|
|
820
|
+
root or one root inside another, so a file has at most one owner.
|
|
821
|
+
|
|
822
|
+
### Import boundary
|
|
823
|
+
|
|
824
|
+
`dist/feature-rules.js` compiles `features` into one dependency-cruiser
|
|
825
|
+
rule per feature, which `.dependency-cruiser.cjs` spreads beside its
|
|
826
|
+
own:
|
|
827
|
+
|
|
828
|
+
```js
|
|
829
|
+
const { featureRules } = require("@avi2dg/checks/dist/feature-rules.js");
|
|
830
|
+
|
|
831
|
+
module.exports = {
|
|
832
|
+
extends: "./node_modules/@avi2dg/checks/dependency-cruiser.config.js",
|
|
833
|
+
forbidden: [...featureRules(require("./quality.json"))],
|
|
834
|
+
};
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
A module outside a feature's root that imports a file inside it must
|
|
838
|
+
import one of the feature's `entries`. Modules under `tests/` and the
|
|
839
|
+
files `allowFrom` matches, such as a CLI or a harness, may import any
|
|
840
|
+
file in it:
|
|
841
|
+
|
|
842
|
+
```
|
|
843
|
+
error feature-billing-entries: src/report.ts → src/billing/charge.ts
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
`featureRules` decodes its argument with the schema the bins use and
|
|
847
|
+
throws the schema's refusal when it does not decode, which stops the
|
|
848
|
+
cruise. It is an ES module, as `effect` is, so a `.cjs` config loads it
|
|
849
|
+
through `require`, which needs node 20.19, 22.12 or later.
|
|
850
|
+
|
|
851
|
+
### Change signal and proof
|
|
852
|
+
|
|
853
|
+
`checks-feature-owners` reads the same declaration over a range:
|
|
854
|
+
|
|
855
|
+
```sh
|
|
856
|
+
checks-feature-owners <base-ref> <head-ref>
|
|
857
|
+
checks-feature-owners <ref>
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
It exits 1 when a feature's proof cannot prove it: the proof or an
|
|
861
|
+
entry is not in the head commit, the proof does not parse, or it
|
|
862
|
+
imports none of the feature's entries. An import counts when it is a
|
|
863
|
+
runtime `import`, `export ... from` or `export * from` of a relative
|
|
864
|
+
path that names an entry: by its own name, by the `.js`, `.jsx`, `.mjs`
|
|
865
|
+
or `.cjs` spelling of it, `.js` naming a `.tsx` entry as well as a
|
|
866
|
+
`.ts` one, or without an extension, the way a directory `index` is
|
|
867
|
+
imported. `import type` does not count, and neither does a
|
|
868
|
+
path alias. The proof runs in `bun run test` like any end-to-end test,
|
|
869
|
+
which is what shows it passes.
|
|
870
|
+
|
|
871
|
+
```
|
|
872
|
+
feature-owners: 1 problem(s) with the features' runnable proofs:
|
|
873
|
+
billing: proof tests/e2e/billing.test.ts imports none of its entries, src/billing/index.ts
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
With `changeSignal` set to `advisory` it also lists each owner the
|
|
877
|
+
range touches, with the paths it touched under the owner's root or at
|
|
878
|
+
its proof, and still exits 0. Whether a change that spans owners is one
|
|
879
|
+
coherent slice is for a reviewer to judge. A rename counts at both of
|
|
880
|
+
its paths:
|
|
881
|
+
|
|
882
|
+
```
|
|
883
|
+
feature-owners: advisory, the range touches 2 feature owner(s); a reviewer judges whether they make one slice:
|
|
884
|
+
billing: src/billing/charge.ts, src/billing/tax.ts
|
|
885
|
+
invoices: src/invoices/tax.ts, tests/e2e/invoices.test.ts
|
|
886
|
+
```
|
|
887
|
+
|
|
888
|
+
It exits 2 when `quality.json` does not decode, which is where a proof
|
|
889
|
+
outside `tests/e2e/` is refused, or a ref does not resolve. A
|
|
890
|
+
repository that declares no feature passes, and `quality.json` refuses
|
|
891
|
+
a `changeSignal` without features, which would map a change to no
|
|
892
|
+
owner.
|
|
893
|
+
|
|
894
|
+
`checks-lint` runs it over each pull request's range; see "Lint entry point".
|
|
895
|
+
|
|
597
896
|
## CI wiring
|
|
598
897
|
|
|
599
898
|
`checks-ci-wiring` fails when a command the repository's CI must run no
|
|
@@ -601,13 +900,17 @@ longer runs on pull requests to the default branch. No local check sees
|
|
|
601
900
|
that: a workflow whose lint step became a no-op leaves `bun run lint`
|
|
602
901
|
green.
|
|
603
902
|
|
|
604
|
-
The repository declares its gates once, in `
|
|
605
|
-
|
|
903
|
+
The repository declares its gates once, in `quality.json`:
|
|
904
|
+
|
|
905
|
+
```json
|
|
906
|
+
"gates": {
|
|
907
|
+
"ci": ["bun run lint", "bun run typecheck", "bun run test"]
|
|
908
|
+
}
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
and `package.json` runs the check through `checks-lint`:
|
|
606
912
|
|
|
607
913
|
```json
|
|
608
|
-
"ciWiring": {
|
|
609
|
-
"gates": ["bun run lint", "bun run typecheck", "bun run test"]
|
|
610
|
-
},
|
|
611
914
|
"scripts": {
|
|
612
915
|
"lint": "oxlint --type-aware && checks-lint"
|
|
613
916
|
}
|
|
@@ -649,11 +952,11 @@ one of the gates `checks-lint` runs by its bare bin name, when the step
|
|
|
649
952
|
calls `checks-lint` the same way: `bunx checks-lint` counts for
|
|
650
953
|
`bunx checks-comment-gate "origin/$BASE_REF" "$HEAD_SHA"`. A step running `bun run lint` counts only for the `bun run lint` gate,
|
|
651
954
|
since the check never reads what a package script runs. So once `lint`
|
|
652
|
-
runs `checks-lint`, the per-gate entries can leave `
|
|
955
|
+
runs `checks-lint`, the per-gate entries can leave `gates.ci`
|
|
653
956
|
along with the workflows that ran them.
|
|
654
957
|
|
|
655
958
|
The default branch is `main`; a repo with another one sets
|
|
656
|
-
`"defaultBranch"`
|
|
959
|
+
`"defaultBranch"` in `quality.json`, which `checks-lint` also reads when
|
|
657
960
|
`origin/HEAD` is not set.
|
|
658
961
|
|
|
659
962
|
It exits 1 naming each gap, with every step that runs the gate and why
|
|
@@ -665,17 +968,17 @@ ci-wiring: 1 of 8 gate(s) do not run on pull requests to main:
|
|
|
665
968
|
.github/workflows/release.yml job publish step 7: .github/workflows/release.yml does not trigger on pull_request
|
|
666
969
|
```
|
|
667
970
|
|
|
668
|
-
It exits 2 when `
|
|
669
|
-
|
|
971
|
+
It exits 2 when `quality.json` declares no `gates.ci` or does not
|
|
972
|
+
decode, a gate or scheduled command is not one plain command, or a
|
|
670
973
|
workflow does not parse. Whether a workflow is well formed is
|
|
671
974
|
actionlint's question, not this one's.
|
|
672
975
|
|
|
673
976
|
A command a schedule must run, such as the flake run, goes in
|
|
674
|
-
`
|
|
977
|
+
`gates.scheduled`:
|
|
675
978
|
|
|
676
979
|
```json
|
|
677
|
-
"
|
|
678
|
-
"
|
|
980
|
+
"gates": {
|
|
981
|
+
"ci": ["bun run lint", "bun run typecheck", "bun run test"],
|
|
679
982
|
"scheduled": ["bunx checks-flake --runs 10 --report flake-report.json"]
|
|
680
983
|
}
|
|
681
984
|
```
|
|
@@ -693,26 +996,30 @@ ci-wiring: 1 of 1 scheduled command(s) do not run on a schedule:
|
|
|
693
996
|
|
|
694
997
|
### Gate selection
|
|
695
998
|
|
|
696
|
-
A repository with no TypeScript source gives `checks-lint-coverage
|
|
697
|
-
`checks-test-layout`
|
|
999
|
+
A repository with no TypeScript source gives `checks-lint-coverage`,
|
|
1000
|
+
`checks-test-layout`, `checks-size-budget` and `checks-feature-owners`
|
|
1001
|
+
nothing to check, and test-layout still refuses its
|
|
698
1002
|
missing `bun test` script and `bunfig.toml`. It declares the gates
|
|
699
|
-
`checks-lint` runs as `
|
|
1003
|
+
`checks-lint` runs as `gates.lint`:
|
|
700
1004
|
|
|
701
1005
|
```json
|
|
702
|
-
"
|
|
703
|
-
"
|
|
704
|
-
"
|
|
1006
|
+
"gates": {
|
|
1007
|
+
"ci": ["bun run lint"],
|
|
1008
|
+
"lint": [
|
|
705
1009
|
"checks-commit-identity",
|
|
706
1010
|
"checks-comment-gate",
|
|
707
1011
|
"checks-suppressions-ratchet",
|
|
708
|
-
"checks-ci-wiring"
|
|
1012
|
+
"checks-ci-wiring",
|
|
1013
|
+
"checks-quality"
|
|
709
1014
|
]
|
|
710
1015
|
}
|
|
711
1016
|
```
|
|
712
1017
|
|
|
713
1018
|
`checks-lint` runs exactly those, in the "Lint entry point" table's
|
|
714
|
-
order, and all
|
|
715
|
-
`
|
|
1019
|
+
order, and all nine when `gates.lint` is absent. A selection in
|
|
1020
|
+
`quality.json` always keeps `checks-quality`, since the file it sits in
|
|
1021
|
+
is what makes that gate apply. A step running
|
|
1022
|
+
`checks-lint` then counts only for a declared gate that `gates.lint`
|
|
716
1023
|
keeps.
|
|
717
1024
|
|
|
718
1025
|
A selection may leave out only a gate that does not apply:
|
|
@@ -725,16 +1032,21 @@ A selection may leave out only a gate that does not apply:
|
|
|
725
1032
|
| `checks-comment-gate` | always |
|
|
726
1033
|
| `checks-suppressions-ratchet` | always |
|
|
727
1034
|
| `checks-ci-wiring` | always |
|
|
1035
|
+
| `checks-quality` | tracks a `quality.json` |
|
|
1036
|
+
| `checks-size-budget` | tracks a `.ts` or `.tsx` file |
|
|
1037
|
+
| `checks-feature-owners` | tracks a `.ts` or `.tsx` file |
|
|
728
1038
|
|
|
729
|
-
Both bins exit 2 on a `
|
|
1039
|
+
Both bins exit 2 on a `gates.lint` that names an unknown gate or leaves
|
|
730
1040
|
out one that always applies. ci-wiring exits 1 when the
|
|
731
1041
|
selection leaves out a gate the repository's tracked files make
|
|
732
1042
|
applicable, and names the gate and the files:
|
|
733
1043
|
|
|
734
1044
|
```
|
|
735
|
-
ci-wiring:
|
|
1045
|
+
ci-wiring: quality.json gates.lint leaves out 4 gate(s) this repository's contents make applicable:
|
|
736
1046
|
checks-lint-coverage: the repository tracks TypeScript source (src/widget.ts)
|
|
737
1047
|
checks-test-layout: the repository tracks TypeScript source (src/widget.ts)
|
|
1048
|
+
checks-size-budget: the repository tracks TypeScript source (src/widget.ts)
|
|
1049
|
+
checks-feature-owners: the repository tracks TypeScript source (src/widget.ts)
|
|
738
1050
|
```
|
|
739
1051
|
|
|
740
1052
|
It reads the files tracked at the checkout, so the pull request that
|
|
@@ -819,9 +1131,12 @@ The shared Stryker preset's `json` reporter writes
|
|
|
819
1131
|
|
|
820
1132
|
## Why it is shaped this way
|
|
821
1133
|
|
|
822
|
-
-
|
|
823
|
-
|
|
824
|
-
|
|
1134
|
+
- Every config in an oxlint `extends` chain brings its own `plugins`,
|
|
1135
|
+
and one that sets none brings oxlint's default plugins, whose
|
|
1136
|
+
category rules the base's `categories` then turn on across the tree.
|
|
1137
|
+
`rules`, `categories` and `jsPlugins` inherit as expected. That is why
|
|
1138
|
+
the consumer snippet restates `plugins` and nothing else, and why the
|
|
1139
|
+
generated fragment always sets them.
|
|
825
1140
|
- `node_modules/` is excluded through the consumer's `.gitignore`, not
|
|
826
1141
|
`ignorePatterns`: oxlint still walks the installed package when only
|
|
827
1142
|
`ignorePatterns` names it.
|
|
@@ -834,10 +1149,32 @@ The shared Stryker preset's `json` reporter writes
|
|
|
834
1149
|
`bun build effect-channel/index.ts --outdir dist --target node --format esm`.
|
|
835
1150
|
Node refuses to type-strip a `.ts` plugin under `node_modules`, so the
|
|
836
1151
|
`.ts` source would fail to load from an installed package.
|
|
1152
|
+
- `featureRules` ships compiled as `dist/feature-rules.js` for the same
|
|
1153
|
+
reason, with `effect` left out of the bundle so it resolves the
|
|
1154
|
+
consumer's own copy. dependency-cruiser uses a config's export as it
|
|
1155
|
+
is and never awaits it, so the declaration decodes synchronously, and
|
|
1156
|
+
`quality.json` exempts that one file from the Effect rules.
|
|
1157
|
+
- `checks-size-budget` writes the head commit's files to a temporary
|
|
1158
|
+
directory and runs oxlint there, with a configuration that sets no
|
|
1159
|
+
plugin and turns every category off, so the consumer's own
|
|
1160
|
+
`.oxlintrc.json`, its ignore files and its other rules never reach the
|
|
1161
|
+
count.
|
|
837
1162
|
- `dist/` is committed. No `prepack` or `prepublishOnly` builds it, so a
|
|
838
1163
|
publish ships whatever bundle the publishing worktree holds. Rebuild it
|
|
839
1164
|
after pulling with `bun run build`; CI fails when the committed bundle
|
|
840
|
-
is stale.
|
|
1165
|
+
is stale. `bun run build` also emits `quality.schema.json`, which is
|
|
1166
|
+
committed the same way, and a test fails when it differs from what
|
|
1167
|
+
the schema emits.
|
|
1168
|
+
- `quality.json` is JSON, not TOML or a TypeScript module: a bun bin, a
|
|
1169
|
+
hook running without `node_modules`, a `.cjs` or `.mjs` config and
|
|
1170
|
+
`jq` all parse it with nothing installed, and nobody runs a
|
|
1171
|
+
repository's own code to learn its policy. It holds declarations
|
|
1172
|
+
only. The kit's bins read it directly; oxlint and tsc read nothing but
|
|
1173
|
+
their own JSON, so they extend generated fragments, which
|
|
1174
|
+
`checks-quality --check` holds to the declarations.
|
|
1175
|
+
- `quality.json` refuses a key its schema does not name, so a kit that
|
|
1176
|
+
cannot enforce a newer key refuses it rather than let the repository
|
|
1177
|
+
believe it enforced.
|
|
841
1178
|
- The base parses with swc because typescript 7 (tsgo) has no compiler
|
|
842
1179
|
API for dependency-cruiser to use. Without `@swc/core` installed the
|
|
843
1180
|
cruise silently skips every `.ts` file, so this repo's test asserts its
|