@avi2dg/checks 0.12.0 → 0.14.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/CHANGELOG.md +124 -0
- package/CONTRIBUTING.md +89 -0
- package/README.md +137 -1230
- package/dist/feature-rules.js +14 -1
- package/docs/configs/commit-messages.md +43 -0
- package/docs/configs/dependency-rules.md +62 -0
- package/docs/configs/effect-rules.md +48 -0
- package/docs/configs/quality-file.md +99 -0
- package/docs/design.md +77 -0
- package/docs/gates/checks-backtest.md +50 -0
- package/docs/gates/checks-ci-wiring.md +122 -0
- package/docs/gates/checks-comment-gate.md +58 -0
- package/docs/gates/checks-commit-identity.md +63 -0
- package/docs/gates/checks-docs.md +98 -0
- package/docs/gates/checks-feature-owners.md +113 -0
- package/docs/gates/checks-flake.md +88 -0
- package/docs/gates/checks-lint-coverage.md +49 -0
- package/docs/gates/checks-lint.md +136 -0
- package/docs/gates/checks-mutation-compare.md +84 -0
- package/docs/gates/checks-quality.md +94 -0
- package/docs/gates/checks-size-budget.md +64 -0
- package/docs/gates/checks-suppressions-ratchet.md +51 -0
- package/docs/gates/checks-test-layout.md +77 -0
- package/docs/gates/checks-test.md +75 -0
- package/package.json +24 -3
- package/quality.schema.json +48 -0
- package/scripts/doc-outline.ts +215 -0
- package/scripts/doc-rules.ts +177 -0
- package/scripts/doc-templates.ts +208 -0
- package/scripts/docs.ts +90 -0
- package/scripts/gates.ts +1 -0
- package/scripts/quality-file.ts +22 -1
- package/templates/adr.md +29 -0
- package/templates/agents.md +17 -0
- package/templates/changelog.md +37 -0
- package/templates/claude.md +2 -0
- package/templates/explanation.md +15 -0
- package/templates/how-to.md +33 -0
- package/templates/readme.md +45 -0
- package/templates/reference.md +15 -0
- package/templates/tutorial.md +31 -0
package/README.md
CHANGED
|
@@ -1,1275 +1,182 @@
|
|
|
1
1
|
# checks
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
that runs the suite and refuses an undeclared skip, the `checks-flake`
|
|
7
|
-
run that records the seeds a failing test fails with, the oxlint base
|
|
8
|
-
config, the tsconfig fragment with the Effect language-service block,
|
|
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
|
|
13
|
-
comment gate with its backtest, the oxlint suppressions ratchet, the
|
|
14
|
-
size budget, the feature-owner change signal and proof check, the
|
|
15
|
-
Stryker mutation-testing preset with its no-regression comparator, the
|
|
16
|
-
CI-wiring check, and the Effect error-channel plugin compiled to
|
|
17
|
-
JavaScript.
|
|
3
|
+
`@avi2dg/checks` is the kit of deterministic checks a TypeScript repository installs to hold its code, tests, commits, CI wiring and docs to one shared standard.
|
|
4
|
+
It ships the lint gates `checks-lint` runs over each pull request, the test runners, and the configs a repository extends for oxlint, tsc, dependency-cruiser, commitlint, bun and Stryker.
|
|
5
|
+
A repository declares what it opts into once, in `quality.json`, and each check decides its constraint the same way on every run.
|
|
18
6
|
|
|
19
|
-
|
|
7
|
+
## Before you begin
|
|
20
8
|
|
|
21
|
-
|
|
9
|
+
<!-- generated prerequisites: bun run build writes it from package.json, .bun-version and scripts/doc-blocks.ts -->
|
|
22
10
|
|
|
23
|
-
|
|
11
|
+
- A git repository, whose history the range gates read.
|
|
12
|
+
- Bun 1.3.13, which runs every bin.
|
|
13
|
+
- TypeScript 7.0.2, whose `tsc` the `typecheck` script runs.
|
|
14
|
+
- The peer dependencies, at the exact versions the kit pins:
|
|
15
|
+
- `@effect/tsgo` 0.45.0
|
|
16
|
+
- `@swc/core` 1.16.2
|
|
17
|
+
- `dependency-cruiser` 18.4.0
|
|
18
|
+
- `effect` 4.0.0-rc.115
|
|
19
|
+
- `oxlint` 1.83.0
|
|
20
|
+
- `oxlint-tsgolint` 7.0.2002
|
|
24
21
|
|
|
25
|
-
|
|
26
|
-
bun add -d @avi2dg/checks effect@4.0.0-rc.115 oxlint@1.83.0 oxlint-tsgolint@7.0.2002 @effect/tsgo@0.45.0 typescript@7.0.2 dependency-cruiser@18.4.0 @swc/core@1.16.2
|
|
27
|
-
```
|
|
22
|
+
<!-- end generated prerequisites -->
|
|
28
23
|
|
|
29
|
-
|
|
24
|
+
## Install
|
|
30
25
|
|
|
31
|
-
|
|
32
|
-
{
|
|
33
|
-
"extends": ["./node_modules/@avi2dg/checks/oxlintrc.json"],
|
|
34
|
-
"plugins": ["typescript", "oxc", "eslint", "import"]
|
|
35
|
-
}
|
|
36
|
-
```
|
|
26
|
+
To consume the kit from a repository:
|
|
37
27
|
|
|
38
|
-
|
|
28
|
+
1. Add the kit and its peers:
|
|
39
29
|
|
|
40
|
-
|
|
41
|
-
node_modules/
|
|
42
|
-
```
|
|
30
|
+
<!-- generated install: bun run build writes it from package.json and scripts/doc-blocks.ts -->
|
|
43
31
|
|
|
44
|
-
|
|
32
|
+
```sh
|
|
33
|
+
bun add -d @avi2dg/checks @effect/tsgo@0.45.0 @swc/core@1.16.2 dependency-cruiser@18.4.0 effect@4.0.0-rc.115 oxlint@1.83.0 oxlint-tsgolint@7.0.2002 typescript@7.0.2
|
|
34
|
+
```
|
|
45
35
|
|
|
46
|
-
|
|
47
|
-
{
|
|
48
|
-
"extends": "@avi2dg/checks/tsconfig.effect.json"
|
|
49
|
-
}
|
|
50
|
-
```
|
|
36
|
+
<!-- end generated install -->
|
|
51
37
|
|
|
52
|
-
|
|
53
|
-
opts into, starting with the commands its CI runs; see "Quality file"
|
|
54
|
-
below:
|
|
38
|
+
1. Extend the oxlint base in `.oxlintrc.json`, restating `plugins`:
|
|
55
39
|
|
|
56
|
-
```json
|
|
57
|
-
{
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
```
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"extends": ["./node_modules/@avi2dg/checks/oxlintrc.json"],
|
|
43
|
+
"plugins": ["typescript", "oxc", "eslint", "import"]
|
|
44
|
+
}
|
|
45
|
+
```
|
|
62
46
|
|
|
63
|
-
|
|
47
|
+
1. Keep oxlint out of `node_modules/` through `.gitignore`:
|
|
64
48
|
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
```
|
|
49
|
+
```
|
|
50
|
+
node_modules/
|
|
51
|
+
```
|
|
68
52
|
|
|
69
|
-
|
|
53
|
+
1. Extend the tsconfig fragment in `tsconfig.json`:
|
|
70
54
|
|
|
71
|
-
```json
|
|
72
|
-
|
|
73
|
-
"
|
|
74
|
-
|
|
75
|
-
```
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"extends": "@avi2dg/checks/tsconfig.effect.json"
|
|
58
|
+
}
|
|
59
|
+
```
|
|
76
60
|
|
|
77
|
-
|
|
78
|
-
repository has not declared; see "Test entry point" below.
|
|
61
|
+
1. Declare the commands CI runs in `quality.json` at the repository root:
|
|
79
62
|
|
|
80
|
-
|
|
81
|
-
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"$schema": "./node_modules/@avi2dg/checks/quality.schema.json",
|
|
66
|
+
"gates": { "ci": ["bun run lint", "bun run typecheck", "bun run test"] }
|
|
67
|
+
}
|
|
68
|
+
```
|
|
82
69
|
|
|
83
|
-
|
|
84
|
-
`.tsx` file, for example through a stray `.gitignore` entry. It compares
|
|
85
|
-
`git ls-files` against oxlint's own file walk and names the missing files.
|
|
86
|
-
It exits 2 when oxlint cannot walk the tree, as when it is not on `PATH`
|
|
87
|
-
or its config does not parse.
|
|
70
|
+
1. Copy the bunfig preset:
|
|
88
71
|
|
|
89
|
-
|
|
72
|
+
```sh
|
|
73
|
+
cp node_modules/@avi2dg/checks/bunfig.toml bunfig.toml
|
|
74
|
+
```
|
|
90
75
|
|
|
91
|
-
|
|
92
|
-
repository owner; see "Commit identity" below.
|
|
76
|
+
1. Add three scripts to `package.json`:
|
|
93
77
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
78
|
+
```json
|
|
79
|
+
"lint": "oxlint --type-aware && checks-lint",
|
|
80
|
+
"typecheck": "tsc --noEmit && effect-tsgo diagnostics --project tsconfig.json --format text --strict",
|
|
81
|
+
"test": "checks-test"
|
|
82
|
+
```
|
|
97
83
|
|
|
98
|
-
|
|
99
|
-
import preset from "@avi2dg/checks/stryker.preset.js";
|
|
84
|
+
1. Run the three on every pull request in `.github/workflows/ci.yml`, fetching the whole history the range needs:
|
|
100
85
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
86
|
+
```yaml
|
|
87
|
+
on:
|
|
88
|
+
pull_request:
|
|
89
|
+
jobs:
|
|
90
|
+
checks:
|
|
91
|
+
runs-on: ubuntu-latest
|
|
92
|
+
steps:
|
|
93
|
+
- uses: actions/checkout@v5
|
|
94
|
+
with:
|
|
95
|
+
fetch-depth: 0
|
|
96
|
+
- uses: oven-sh/setup-bun@v2
|
|
97
|
+
- run: bun install --frozen-lockfile
|
|
98
|
+
- run: bun run lint
|
|
99
|
+
- run: bun run typecheck
|
|
100
|
+
- run: bun run test
|
|
101
|
+
```
|
|
105
102
|
|
|
106
|
-
|
|
107
|
-
`@avi2dg/checks` to adopt a new release.
|
|
103
|
+
`bun run lint` then ends with `checks-lint: <count> gate(s) pass`.
|
|
108
104
|
|
|
109
|
-
##
|
|
105
|
+
## What runs
|
|
110
106
|
|
|
111
|
-
`
|
|
112
|
-
|
|
107
|
+
`checks-lint` runs these gates in this order, each over the working tree or over the range it resolves, and names every one that fails.
|
|
108
|
+
A repository leaves out a gate that does not apply to it through `gates.lint`, as [Gate selection](docs/gates/checks-lint.md#gate-selection) says.
|
|
113
109
|
|
|
114
|
-
|
|
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
|
-
```
|
|
110
|
+
<!-- generated gates: bun run build writes it from KIT_GATES in scripts/gates.ts and scripts/doc-blocks.ts -->
|
|
141
111
|
|
|
142
|
-
|
|
|
112
|
+
| Gate | Reads | Runs in |
|
|
143
113
|
| --- | --- | --- |
|
|
144
|
-
| `
|
|
145
|
-
| `gates
|
|
146
|
-
| `gates
|
|
147
|
-
| `
|
|
148
|
-
| `
|
|
149
|
-
| `
|
|
150
|
-
| `
|
|
151
|
-
| `
|
|
152
|
-
| `
|
|
153
|
-
| `
|
|
154
|
-
| `agentRules.on`, `agentRules.off` | agent Rule selection, not the kit | catalogued Rules switched on or off for this repository |
|
|
114
|
+
| [`checks-lint-coverage`](docs/gates/checks-lint-coverage.md) | the working tree | a repository tracking `*.ts` or `*.tsx` |
|
|
115
|
+
| [`checks-test-layout`](docs/gates/checks-test-layout.md) | the working tree | a repository tracking `*.ts` or `*.tsx` |
|
|
116
|
+
| [`checks-commit-identity`](docs/gates/checks-commit-identity.md) | the range | every repository |
|
|
117
|
+
| [`checks-comment-gate`](docs/gates/checks-comment-gate.md) | the range | every repository |
|
|
118
|
+
| [`checks-suppressions-ratchet`](docs/gates/checks-suppressions-ratchet.md) | the range | every repository |
|
|
119
|
+
| [`checks-ci-wiring`](docs/gates/checks-ci-wiring.md) | the working tree | every repository |
|
|
120
|
+
| [`checks-docs`](docs/gates/checks-docs.md) | the range | every repository |
|
|
121
|
+
| [`checks-quality`](docs/gates/checks-quality.md) | the working tree | a repository tracking `quality.json` |
|
|
122
|
+
| [`checks-size-budget`](docs/gates/checks-size-budget.md) | the range | a repository tracking `*.ts` or `*.tsx` |
|
|
123
|
+
| [`checks-feature-owners`](docs/gates/checks-feature-owners.md) | the range | a repository tracking `*.ts` or `*.tsx` |
|
|
155
124
|
|
|
156
|
-
|
|
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.
|
|
125
|
+
<!-- end generated gates -->
|
|
173
126
|
|
|
174
|
-
|
|
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:
|
|
127
|
+
These bins run on their own:
|
|
178
128
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
```
|
|
129
|
+
- [`checks-test`](docs/gates/checks-test.md) runs the suite as `scripts.test` and refuses a skip the repository has not declared.
|
|
130
|
+
- [`checks-flake`](docs/gates/checks-flake.md) runs the suite on a schedule and records the seeds a flaky test fails with.
|
|
131
|
+
- [`checks-mutation-compare`](docs/gates/checks-mutation-compare.md) holds a pull request's mutation score to no regression.
|
|
132
|
+
- [`checks-backtest`](docs/gates/checks-backtest.md) reports what the comment check would have refused in recent history.
|
|
207
133
|
|
|
208
|
-
`
|
|
134
|
+
`checks-lint` has [its own page](docs/gates/checks-lint.md), which says which range it resolves.
|
|
135
|
+
The oxlint base, the dependency-cruiser base and the commitlint config run through their own tools, as the pages under Related topics say.
|
|
209
136
|
|
|
210
|
-
|
|
211
|
-
{
|
|
212
|
-
"extends": ["@avi2dg/checks/tsconfig.effect.json", "./tsconfig.quality.json"]
|
|
213
|
-
}
|
|
214
|
-
```
|
|
137
|
+
## Upgrade
|
|
215
138
|
|
|
216
|
-
|
|
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.
|
|
139
|
+
To move a repository to a newer release of the kit:
|
|
224
140
|
|
|
225
|
-
|
|
141
|
+
1. Run the install line again, which moves the kit to its newest release and the peers to the versions it pins.
|
|
142
|
+
1. Run `bun run checks-quality generate` when `quality.json` declares `sources.effect`, since a release that changes a preset reaches the fragments only through it.
|
|
143
|
+
1. Copy `node_modules/@avi2dg/checks/bunfig.toml` over `bunfig.toml` again, since `checks-test-layout` compares the copy with the installed preset.
|
|
144
|
+
1. Run `bun run lint`, `bun run typecheck` and `bun run test`.
|
|
226
145
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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.
|
|
146
|
+
The repository's lockfile pins the kit, so a repository moves only when it runs these steps.
|
|
147
|
+
[CHANGELOG.md](CHANGELOG.md), shipped in the package, lists what each release changed.
|
|
148
|
+
A repository that still declares `ciWiring` or `commitIdentity` in `package.json` moves them into `quality.json`, as [Keys moved from package.json](docs/configs/quality-file.md#keys-moved-from-packagejson) maps.
|
|
236
149
|
|
|
237
|
-
|
|
238
|
-
fragments, removes a left-over one, then runs the same check.
|
|
150
|
+
## Where things are
|
|
239
151
|
|
|
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
|
-
```
|
|
152
|
+
Every path is relative to the installed package, `node_modules/@avi2dg/checks/`.
|
|
245
153
|
|
|
246
|
-
|
|
247
|
-
pin both:
|
|
154
|
+
<!-- generated shipped: bun run build writes it from package.json and scripts/doc-blocks.ts -->
|
|
248
155
|
|
|
249
|
-
|
|
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
|
-
|
|
261
|
-
## Lint entry point
|
|
262
|
-
|
|
263
|
-
`checks-lint` runs each of the kit's lint gates in turn and names every
|
|
264
|
-
one that fails, rather than stopping at the first. A repository whose
|
|
265
|
-
tracked files give a gate nothing to check can leave it out through
|
|
266
|
-
`gates.lint`; see "Gate selection" under "CI wiring".
|
|
267
|
-
|
|
268
|
-
| Gate | Reads |
|
|
156
|
+
| Path | What it holds |
|
|
269
157
|
| --- | --- |
|
|
270
|
-
| `
|
|
271
|
-
| `
|
|
272
|
-
| `
|
|
273
|
-
| `
|
|
274
|
-
| `
|
|
275
|
-
| `
|
|
276
|
-
| `
|
|
277
|
-
| `
|
|
278
|
-
| `checks-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
base branch is read from the fetch, not from the event's recorded base
|
|
296
|
-
sha, which GitHub leaves stale once the base branch advances after the
|
|
297
|
-
pull request opens. Explicit base and head arguments override both.
|
|
298
|
-
|
|
299
|
-
The range always starts at the merge base, never at the base branch's
|
|
300
|
-
tip: commits the base branch gained after the head branched off would
|
|
301
|
-
otherwise count against the head. When the head is the merge base, as
|
|
302
|
-
on a push to the default branch or a local run on it, the range would be
|
|
303
|
-
empty, so each range gate is handed that tip commit alone and checks it
|
|
304
|
-
against its parent, or against the empty tree when it is a repository's
|
|
305
|
-
first commit:
|
|
306
|
-
|
|
307
|
-
```
|
|
308
|
-
checks-lint: tip 10ba7d8935b73ed72624120a1542e51bd21ca7c7 from HEAD against origin/main
|
|
309
|
-
```
|
|
310
|
-
|
|
311
|
-
A clone with no remote-tracking refs at all, such as a freshly
|
|
312
|
-
initialised repository with no remote or one whose remote was never
|
|
313
|
-
fetched, has no default branch to start from, so each range gate is
|
|
314
|
-
handed `HEAD` alone the same way:
|
|
315
|
-
|
|
316
|
-
```
|
|
317
|
-
checks-lint: tip 10ba7d8935b73ed72624120a1542e51bd21ca7c7 from HEAD alone, as the clone has no remote-tracking refs
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
Once any ref sits under `refs/remotes/`, a missing
|
|
321
|
-
`origin/<default branch>` exits 2 instead, since that is the shape of a
|
|
322
|
-
shallow CI checkout, where `HEAD` alone would leave the commits before
|
|
323
|
-
it unchecked.
|
|
324
|
-
|
|
325
|
-
It prints the range, the declared selection if there is one, each
|
|
326
|
-
gate's own report, then its verdict:
|
|
327
|
-
|
|
328
|
-
```
|
|
329
|
-
checks-lint: range 2504acf098d120e73a8ece3c96f22b934f35c6a8..10ba7d8935b73ed72624120a1542e51bd21ca7c7 from HEAD against origin/main
|
|
330
|
-
...
|
|
331
|
-
checks-lint: 3 of 9 gate(s) failed: checks-commit-identity, checks-comment-gate, checks-suppressions-ratchet
|
|
332
|
-
```
|
|
333
|
-
|
|
334
|
-
It exits 1 when any gate found a violation, and 2 when the range or the
|
|
335
|
-
selection does not resolve, or no failing gate could decide. ci-wiring
|
|
336
|
-
always runs, so a repository on `checks-lint` declares `gates.ci`
|
|
337
|
-
(see "CI wiring"), and it holds the test layout unless its selection
|
|
338
|
-
leaves out `checks-test-layout`.
|
|
339
|
-
|
|
340
|
-
CI runs it through `lint`. The checkout fetches the whole history, which
|
|
341
|
-
the merge base needs:
|
|
342
|
-
|
|
343
|
-
```yaml
|
|
344
|
-
on:
|
|
345
|
-
pull_request:
|
|
346
|
-
jobs:
|
|
347
|
-
lint:
|
|
348
|
-
runs-on: ubuntu-latest
|
|
349
|
-
steps:
|
|
350
|
-
- uses: actions/checkout@v5
|
|
351
|
-
with:
|
|
352
|
-
fetch-depth: 0
|
|
353
|
-
- uses: oven-sh/setup-bun@v2
|
|
354
|
-
- run: bun install --frozen-lockfile
|
|
355
|
-
- run: bun run lint
|
|
356
|
-
```
|
|
357
|
-
|
|
358
|
-
## Effect rules
|
|
359
|
-
|
|
360
|
-
The base config loads the `effect-channel` plugin and turns on
|
|
361
|
-
`effect-channel/no-error-channel-escape`, which refuses `Effect.ignore`,
|
|
362
|
-
`Effect.ignoreCause`, the `Effect.catchCause` family, and an
|
|
363
|
-
`Effect.catch` whose handler takes no error or names it `_`.
|
|
364
|
-
|
|
365
|
-
Two more rules ship off, because a repository writes only some of its
|
|
366
|
-
paths in Effect: code a host loads without `node_modules`, such as a
|
|
367
|
-
hook bundle or this oxlint plugin, cannot import it.
|
|
368
|
-
|
|
369
|
-
- `effect-channel/no-throw` refuses a `throw` statement.
|
|
370
|
-
- `effect-channel/no-try-catch` refuses a `try` statement with a
|
|
371
|
-
`catch` clause. `try`/`finally` stays allowed.
|
|
372
|
-
|
|
373
|
-
Each refusal says what to write instead: a `Schema.TaggedError` failed
|
|
374
|
-
through `Effect.fail`, a throwing call wrapped in `Effect.try` or
|
|
375
|
-
`Effect.tryPromise`, and recovery by tag with `Effect.catchTag`.
|
|
376
|
-
|
|
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":
|
|
380
|
-
|
|
381
|
-
```json
|
|
382
|
-
"sources": {
|
|
383
|
-
"effect": { "paths": ["src/**/*.ts"], "exempt": ["src/host/*.ts"] }
|
|
384
|
-
}
|
|
385
|
-
```
|
|
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
|
-
|
|
392
|
-
oxlint resolves `files` against the directory of the config that holds
|
|
393
|
-
the override, so a config passed with `-c` from outside the repository
|
|
394
|
-
matches nothing and reports nothing.
|
|
395
|
-
|
|
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`.
|
|
407
|
-
|
|
408
|
-
The language service holds the same paths to Effect-native IO through
|
|
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.
|
|
414
|
-
|
|
415
|
-
## Test layout
|
|
416
|
-
|
|
417
|
-
`checks-test-layout` fails unless the repo holds this shape, and names the
|
|
418
|
-
file and the path to move it to when it does not:
|
|
419
|
-
|
|
420
|
-
- Every test file is `tests/**/*.test.ts` or `.tsx`. A `*.test.ts`, `*.spec.ts` or
|
|
421
|
-
`*_test.ts` under `src/`, `test/`, `__tests__/` or the repo root fails.
|
|
422
|
-
Tracked and untracked files that `git ls-files --exclude-standard`
|
|
423
|
-
reports are scanned, so `node_modules/` and every gitignored tree are
|
|
424
|
-
out of reach, and a local run agrees with CI before `git add`.
|
|
425
|
-
- `tests/lib/**` holds helpers and `tests/fixtures/**` holds data; neither
|
|
426
|
-
may hold a test file. Every other directory directly under `tests/` is a
|
|
427
|
-
test group and may nest as deep as it likes.
|
|
428
|
-
- Two levels. A test outside `tests/e2e/` runs in-process, so it may not
|
|
429
|
-
import `node:child_process`, `net`, `http`, `https`, `http2`, `tls` or
|
|
430
|
-
`dgram`, may not import `$`, `spawn`, `spawnSync`, `connect`, `serve` or
|
|
431
|
-
`listen` from `bun`, may not touch `Bun.$` or `Bun.spawn`, and may not
|
|
432
|
-
call `fetch`. A test inside `tests/e2e/` may do all of it. Helpers in
|
|
433
|
-
`tests/lib/**` answer to the same rule, since an in-process test reaches
|
|
434
|
-
them; `tests/fixtures/**` is data and is not parsed. Detection parses
|
|
435
|
-
with swc and reads import specifiers and identifier use, so a test that
|
|
436
|
-
only carries `"node:child_process"` as a string is not a violation.
|
|
437
|
-
- `scripts.test` is exactly `checks-test`, which runs `bun test --randomize`
|
|
438
|
-
(see "Test entry point"), and `scripts.lint` runs this check, itself or
|
|
439
|
-
through `checks-lint` called by its bare bin name.
|
|
440
|
-
- `bunfig.toml` carries every `[test]` key of the shipped preset with the
|
|
441
|
-
same value, and `[test].pathIgnorePatterns` is always
|
|
442
|
-
`["**/tests/quarantine/**"]`: the check pins it itself, so this repo,
|
|
443
|
-
whose bunfig is the preset, cannot drift it either. Other tables, and
|
|
444
|
-
extra `[test]` keys, are the repo's own.
|
|
445
|
-
|
|
446
|
-
The in-process half is what a mutation run can mutate; `tests/e2e/**` is
|
|
447
|
-
excluded from a mutate scope by construction, because a subprocess kills
|
|
448
|
-
both the speed and the coverage signal a mutant needs.
|
|
449
|
-
|
|
450
|
-
The preset also skips `tests/quarantine/**` on a default run. A test that
|
|
451
|
-
turns flaky moves there, so the suite stays trustworthy, and the flake is
|
|
452
|
-
still run on demand:
|
|
453
|
-
|
|
454
|
-
```sh
|
|
455
|
-
bun test --path-ignore-patterns='' tests/quarantine
|
|
456
|
-
```
|
|
457
|
-
|
|
458
|
-
## Test entry point
|
|
459
|
-
|
|
460
|
-
`checks-test` runs the whole suite with `bun test --randomize`, passes
|
|
461
|
-
bun's output through, and then reads bun's JUnit report of the same run.
|
|
462
|
-
bun exits 0 with tests skipped, so a green run says nothing about the
|
|
463
|
-
tests that never ran. `checks-test` fails when a test was skipped, by
|
|
464
|
-
`test.skip`, `test.skipIf`, `test.if`, `describe.skip` or `test.todo`,
|
|
465
|
-
without a declaration in `package.json`:
|
|
466
|
-
|
|
467
|
-
```json
|
|
468
|
-
"testSkips": [
|
|
469
|
-
{
|
|
470
|
-
"file": "tests/e2e/docker.test.ts",
|
|
471
|
-
"test": "images > builds the release image",
|
|
472
|
-
"reason": "the runner has no docker daemon",
|
|
473
|
-
"when": "ci"
|
|
474
|
-
}
|
|
475
|
-
]
|
|
476
|
-
```
|
|
477
|
-
|
|
478
|
-
`file` is the path bun reports, relative to the package root, and `test`
|
|
479
|
-
is the name bun's console prints: the describe blocks and the test name
|
|
480
|
-
joined by ` > `. `reason` is required. `when` is `ci` or `local` for a
|
|
481
|
-
test skipped only there, and a declaration without it holds in both;
|
|
482
|
-
`checks-test` counts a run as `ci` when `CI` is set true, as GitHub
|
|
483
|
-
Actions sets it. A declaration that holds for the run but matches no
|
|
484
|
-
skipped test fails a ci run too, so a fixed or renamed test takes its
|
|
485
|
-
declaration with it. A local run only warns about it, because whether a
|
|
486
|
-
test skips there can hang on the machine, such as a docker daemon being
|
|
487
|
-
up:
|
|
488
|
-
|
|
489
|
-
```
|
|
490
|
-
checks-test: 1 skipped test(s) undeclared and 1 declaration(s) matching no skipped test in this ci run:
|
|
491
|
-
tests/pricing.test.ts:12 pricing > rounds half to even: skipped with no declaration; run it, or declare it in package.json testSkips with its reason
|
|
492
|
-
tests/e2e/docker.test.ts > images > builds the release image: declared, but no such test skipped; delete the declaration
|
|
493
|
-
```
|
|
494
|
-
|
|
495
|
-
It exits 1 when a test failed or a skip is undeclared or, in a ci run,
|
|
496
|
-
a declaration stale, and 2 when `testSkips` does not parse or bun
|
|
497
|
-
passed without writing its report. It takes no arguments: a `-t`
|
|
498
|
-
filter reports every test it leaves out as skipped and a path filter
|
|
499
|
-
drops files a declaration names, so a narrowed run is plain
|
|
500
|
-
`bun test --randomize` with the arguments. Files under
|
|
501
|
-
`tests/quarantine/` are never run and so never reported; see "Test
|
|
502
|
-
layout".
|
|
503
|
-
|
|
504
|
-
## Flake run
|
|
505
|
-
|
|
506
|
-
A green run proves nothing failed in that run, not that no test is
|
|
507
|
-
flaky. `checks-flake` runs the whole suite several times, each with its
|
|
508
|
-
own `--seed`, and records per failing test the seeds it failed with:
|
|
509
|
-
|
|
510
|
-
```sh
|
|
511
|
-
checks-flake [--runs <count> | --seed <seed>...] [--report <file>]
|
|
512
|
-
```
|
|
513
|
-
|
|
514
|
-
`--runs` defaults to 10 runs on random seeds, and `--seed`, given once
|
|
515
|
-
per run, replays chosen seeds, such as the ones a report recorded.
|
|
516
|
-
`bun test --randomize --seed=<seed>` puts the suite in the same order,
|
|
517
|
-
so a seed reproduces a failure that hangs on order. `--report` writes the
|
|
518
|
-
record as JSON, every run's seed and failing tests and every failing
|
|
519
|
-
test's seeds, and under GitHub Actions the summary below is appended to
|
|
520
|
-
the job summary:
|
|
521
|
-
|
|
522
|
-
```
|
|
523
|
-
checks-flake: 3 of 10 run(s) failed, 1 test(s) failing in them
|
|
524
|
-
|
|
525
|
-
| Test | Failed | Seeds |
|
|
526
|
-
| --- | --- | --- |
|
|
527
|
-
| tests/cache.test.ts:6 reads the cache | 3 of 10 runs | 2170533150, 4046124386, 180394251 |
|
|
528
|
-
|
|
529
|
-
Reproduce a failing run with bun test --randomize --seed=<seed>.
|
|
530
|
-
```
|
|
531
|
-
|
|
532
|
-
A run that fails with no failing test, such as a test file that throws
|
|
533
|
-
while loading, is listed with its seed on its own line. It exits 1 when
|
|
534
|
-
any run failed and 2 when bun passed without writing its report.
|
|
535
|
-
|
|
536
|
-
A consumer runs it on a schedule and keeps the record as an artifact:
|
|
537
|
-
|
|
538
|
-
```yaml
|
|
539
|
-
on:
|
|
540
|
-
schedule:
|
|
541
|
-
- cron: "17 5 * * *"
|
|
542
|
-
workflow_dispatch:
|
|
543
|
-
jobs:
|
|
544
|
-
flake:
|
|
545
|
-
runs-on: ubuntu-latest
|
|
546
|
-
steps:
|
|
547
|
-
- uses: actions/checkout@v5
|
|
548
|
-
- uses: oven-sh/setup-bun@v2
|
|
549
|
-
- run: bun install --frozen-lockfile
|
|
550
|
-
- run: bunx checks-flake --runs 10 --report flake-report.json
|
|
551
|
-
- uses: actions/upload-artifact@v4
|
|
552
|
-
if: always()
|
|
553
|
-
with:
|
|
554
|
-
name: flake-report
|
|
555
|
-
path: flake-report.json
|
|
556
|
-
```
|
|
557
|
-
|
|
558
|
-
and declares the step in `gates.scheduled`, so `checks-ci-wiring`
|
|
559
|
-
fails once the schedule stops running it; see "CI wiring".
|
|
560
|
-
|
|
561
|
-
## Dependency rules
|
|
562
|
-
|
|
563
|
-
`.dependency-cruiser.cjs` extends the shared base, which carries
|
|
564
|
-
`no-circular`, `no-orphans`, `not-to-dev-dep` (shipped source importing
|
|
565
|
-
a dev-only package, which a package listed in `peerDependencies` too is
|
|
566
|
-
not), `not-to-unresolvable` (nothing installed answers the
|
|
567
|
-
specifier), and `no-deep-imports` (a subpath the package's exports map
|
|
568
|
-
does not publish):
|
|
569
|
-
|
|
570
|
-
```js
|
|
571
|
-
module.exports = {
|
|
572
|
-
extends: "./node_modules/@avi2dg/checks/dependency-cruiser.config.js",
|
|
573
|
-
forbidden: [
|
|
574
|
-
{
|
|
575
|
-
name: "ui-cannot-reach-server",
|
|
576
|
-
severity: "error",
|
|
577
|
-
from: { path: "^src/ui" },
|
|
578
|
-
to: { path: "^src/server" },
|
|
579
|
-
},
|
|
580
|
-
],
|
|
581
|
-
};
|
|
582
|
-
```
|
|
583
|
-
|
|
584
|
-
That last block is the layer-boundary recipe: append a named rule per
|
|
585
|
-
boundary you own. A rule that restates a base name overrides it field
|
|
586
|
-
by field, which is how an entry point stops being an orphan:
|
|
587
|
-
redeclare `no-orphans` with your entry added to its `pathNot`.
|
|
588
|
-
This repo's own `.dependency-cruiser.cjs` does that for the plugin
|
|
589
|
-
entry. A repository that declares feature owners spreads the rules
|
|
590
|
-
`quality.json` compiles to into the same `forbidden`; see "Feature
|
|
591
|
-
owners".
|
|
592
|
-
|
|
593
|
-
`package.json` gains the script:
|
|
594
|
-
|
|
595
|
-
```json
|
|
596
|
-
"lint:deps": "depcruise --config .dependency-cruiser.cjs src"
|
|
597
|
-
```
|
|
598
|
-
|
|
599
|
-
Enforcement runs in CI beside the other checks:
|
|
600
|
-
|
|
601
|
-
```yaml
|
|
602
|
-
jobs:
|
|
603
|
-
lint:
|
|
604
|
-
steps:
|
|
605
|
-
- uses: actions/checkout@v5
|
|
606
|
-
- uses: oven-sh/setup-bun@v2
|
|
607
|
-
- run: bun install --frozen-lockfile
|
|
608
|
-
- run: bun run lint:deps
|
|
609
|
-
```
|
|
610
|
-
|
|
611
|
-
## Commit lint
|
|
612
|
-
|
|
613
|
-
Commits follow `@commitlint/config-conventional` plus the house
|
|
614
|
-
prefixes listed in `commitlint.config.js`, shared from
|
|
615
|
-
`@avi2dg/checks/commitlint.config.js`. It arrives with the registry
|
|
616
|
-
dependency above, since `@commitlint/cli` and
|
|
617
|
-
`@commitlint/config-conventional` are dependencies, not peers.
|
|
618
|
-
Enforcement runs in CI on pull requests, because `jj` never fires a
|
|
619
|
-
git hook. Add this workflow to the consuming repo:
|
|
620
|
-
|
|
621
|
-
```yaml
|
|
622
|
-
on:
|
|
623
|
-
pull_request:
|
|
624
|
-
types: [opened, edited, synchronize, reopened]
|
|
625
|
-
jobs:
|
|
626
|
-
commitlint:
|
|
627
|
-
runs-on: ubuntu-latest
|
|
628
|
-
steps:
|
|
629
|
-
- uses: actions/checkout@v5
|
|
630
|
-
- uses: oven-sh/setup-bun@v2
|
|
631
|
-
- run: bun install --frozen-lockfile
|
|
632
|
-
- run: printf '%s' "$PR_TITLE (#0000)" > "$RUNNER_TEMP/pr-title"
|
|
633
|
-
env:
|
|
634
|
-
PR_TITLE: ${{ github.event.pull_request.title }}
|
|
635
|
-
- run: ./node_modules/.bin/commitlint --config ./node_modules/@avi2dg/checks/commitlint.config.js --edit "$RUNNER_TEMP/pr-title"
|
|
636
|
-
```
|
|
637
|
-
|
|
638
|
-
It lints the pull request title and nothing else. The title is the
|
|
639
|
-
enforced subject because squash merges use the PR title as the main
|
|
640
|
-
commit subject; per-commit messages are not linted. GitHub appends
|
|
641
|
-
` (#N)` to the squashed subject, so the workflow lints the title with
|
|
642
|
-
that suffix attached and the header length limit applies to the landed
|
|
643
|
-
subject, not the bare title.
|
|
644
|
-
|
|
645
|
-
It never sees a commit's author or committer fields, nor the
|
|
646
|
-
`Co-authored-by` trailer GitHub writes from a foreign author when it
|
|
647
|
-
squashes, so it cannot enforce who a commit belongs to. The
|
|
648
|
-
commit-identity check below is the enforcement.
|
|
649
|
-
|
|
650
|
-
## Commit identity
|
|
651
|
-
|
|
652
|
-
`scripts/commit-identity.ts` walks every commit in a range and fails when
|
|
653
|
-
one carries an identity other than the repository owner's:
|
|
654
|
-
|
|
655
|
-
```sh
|
|
656
|
-
bun run checks-commit-identity <base-ref> <head-ref>
|
|
657
|
-
bun run checks-commit-identity <ref>
|
|
658
|
-
```
|
|
659
|
-
|
|
660
|
-
With one argument it checks that commit alone. It refuses a commit
|
|
661
|
-
whose author or committer is outside the allowlist, and one whose
|
|
662
|
-
trailer block carries a `Co-authored-by:` trailer as git parses it, and
|
|
663
|
-
it names the offending commit and reason. Other trailers and prose mentioning an address in the
|
|
664
|
-
body are left alone. `GitHub <noreply@github.com>` is allowed as
|
|
665
|
-
committer only, since that is who writes a squash merge.
|
|
666
|
-
|
|
667
|
-
The allowlist defaults to `avi2d <avi2dg@gmail.com>`. A repo with other
|
|
668
|
-
owners restates it in `quality.json`:
|
|
669
|
-
|
|
670
|
-
```json
|
|
671
|
-
"commitIdentity": {
|
|
672
|
-
"authors": [{ "name": "avi2d", "email": "avi2dg@gmail.com" }]
|
|
673
|
-
}
|
|
674
|
-
```
|
|
675
|
-
|
|
676
|
-
`checks-lint` runs it over each pull request's range; see "Lint entry point".
|
|
677
|
-
|
|
678
|
-
## Comment gate
|
|
679
|
-
|
|
680
|
-
`scripts/comment-gate.ts` runs the comment check over a diff and fails
|
|
681
|
-
when an added line carries a banned comment:
|
|
682
|
-
|
|
683
|
-
```sh
|
|
684
|
-
bun run checks-comment-gate <base-ref> <head-ref>
|
|
685
|
-
bun run checks-comment-gate <ref>
|
|
686
|
-
```
|
|
687
|
-
|
|
688
|
-
With two arguments it diffs the base against the head. With one it diffs
|
|
689
|
-
that commit against its parent, or against the empty tree for a
|
|
690
|
-
repository's first commit, which has none. It exits 2 when the parent
|
|
691
|
-
exists but is not in the clone rather than widening to the whole tree,
|
|
692
|
-
so a shallow checkout running the one-argument form needs
|
|
693
|
-
`fetch-depth: 2`. Only added lines
|
|
694
|
-
are checked, so a violation in a file the diff never touches stays
|
|
695
|
-
silent, and a refusal counts when any line of the comment carrying it was
|
|
696
|
-
added. The check refuses a machine-read directive, a record or ticket
|
|
697
|
-
pointer, a doc block, and a file opening with a rationale block over
|
|
698
|
-
three lines, licence headers excepted.
|
|
699
|
-
|
|
700
|
-
`scripts/comment-matchers.ts` holds the scanner, the comment syntaxes
|
|
701
|
-
and a synchronous `refused()`, and imports nothing, so a host such as a
|
|
702
|
-
hook bundle can copy it alone into a directory with no `node_modules`
|
|
703
|
-
and import it as `@avi2dg/checks/scripts/comment-matchers.ts`. The repo's
|
|
704
|
-
cruise fails when it gains an import. `scripts/comments.ts` wraps the
|
|
705
|
-
same matchers in Effect for the gate and the backtest.
|
|
706
|
-
|
|
707
|
-
`checks-lint` runs it over each pull request's range; see "Lint entry point".
|
|
708
|
-
|
|
709
|
-
## Suppressions ratchet
|
|
710
|
-
|
|
711
|
-
`scripts/suppressions-ratchet.ts` holds oxlint's bulk-suppression
|
|
712
|
-
baseline, `oxlint-suppressions.json`, to counts that only fall. oxlint
|
|
713
|
-
accepts whatever `oxlint --suppress-all` writes, so raising a count to
|
|
714
|
-
let a new site through passes the lint. The ratchet reads the file at a
|
|
715
|
-
base and a head and fails naming each file and rule whose count rose or
|
|
716
|
-
that appeared:
|
|
717
|
-
|
|
718
|
-
```sh
|
|
719
|
-
bun run checks-suppressions-ratchet <base-ref> <head-ref>
|
|
720
|
-
bun run checks-suppressions-ratchet <ref>
|
|
721
|
-
```
|
|
722
|
-
|
|
723
|
-
With one argument it compares that commit with its parent, or with the
|
|
724
|
-
empty tree for a repository's first commit, and exits 2 when the parent
|
|
725
|
-
exists but is not in the clone. With two it reads the base where
|
|
726
|
-
the head branched off, at their merge-base, so a count the base branch
|
|
727
|
-
lowered since does not read as a rise at the head. A count that fell and
|
|
728
|
-
an entry that went both pass. A commit without the file counts as empty,
|
|
729
|
-
so the commit that first adds a baseline fails with every entry
|
|
730
|
-
appearing. The file is read from the directory the command runs in,
|
|
731
|
-
which is where oxlint writes it. It exits 2 when a ref does not resolve
|
|
732
|
-
or the file is not oxlint's count per rule per file:
|
|
733
|
-
|
|
734
|
-
```
|
|
735
|
-
suppressions-ratchet: 2 count(s) in oxlint-suppressions.json rose or appeared; fix the site instead of suppressing it:
|
|
736
|
-
src/added.ts typescript/no-unsafe-type-assertion appeared with 1
|
|
737
|
-
src/dispatch.ts typescript/no-non-null-assertion rose from 12 to 13
|
|
738
|
-
```
|
|
739
|
-
|
|
740
|
-
`checks-lint` runs it over each pull request's range; see "Lint entry point".
|
|
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
|
-
|
|
896
|
-
## CI wiring
|
|
897
|
-
|
|
898
|
-
`checks-ci-wiring` fails when a command the repository's CI must run no
|
|
899
|
-
longer runs on pull requests to the default branch. No local check sees
|
|
900
|
-
that: a workflow whose lint step became a no-op leaves `bun run lint`
|
|
901
|
-
green.
|
|
902
|
-
|
|
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`:
|
|
912
|
-
|
|
913
|
-
```json
|
|
914
|
-
"scripts": {
|
|
915
|
-
"lint": "oxlint --type-aware && checks-lint"
|
|
916
|
-
}
|
|
917
|
-
```
|
|
918
|
-
|
|
919
|
-
It parses every `.github/workflows/*.yml` and `*.yaml` and looks, for
|
|
920
|
-
each gate, for a `run:` step that is the gate command alone on one line,
|
|
921
|
-
optionally followed by plain arguments: words, quoted strings, and
|
|
922
|
-
`$VAR` or `${VAR}` expansions. `bun run lint --quiet` and
|
|
923
|
-
`bunx checks-comment-gate "origin/$BASE_REF" "$HEAD_SHA"` count;
|
|
924
|
-
`bun run lint:deps`, `echo bun run lint` and a step `name:` do not. A
|
|
925
|
-
step whose script has a second line, or any `|`, `||`, `&&`, `;`, `&`,
|
|
926
|
-
`$(...)`, backticks, `<` or `>` redirection, a comment or a leading
|
|
927
|
-
`NAME=value`, never counts, because each can run the gate without its
|
|
928
|
-
failure failing the step; the report names the gate and says to give it
|
|
929
|
-
its own step with nothing else in it. A gate step counts only when:
|
|
930
|
-
|
|
931
|
-
- its workflow triggers on `pull_request`, any `branches` or
|
|
932
|
-
`branches-ignore` filter there keeps the default branch, any `types`
|
|
933
|
-
filter keeps `opened` and `synchronize`, and it sets no `paths` or
|
|
934
|
-
`paths-ignore` filter, which lets some pull requests skip the gate;
|
|
935
|
-
- neither the step nor its job sets `if: false` or
|
|
936
|
-
`continue-on-error: true`, bare or as `${{ false }}` and `${{ true }}`;
|
|
937
|
-
- its job needs no job, directly or through a chain, that sets
|
|
938
|
-
`if: false`, unless a job on that chain has an `if:` calling
|
|
939
|
-
`always()`, `failure()` or `cancelled()`. GitHub prefixes every other
|
|
940
|
-
`if:`, including `true` and `success()`, with `success()`, so a job
|
|
941
|
-
whose needed job was skipped is skipped too.
|
|
942
|
-
|
|
943
|
-
A job calling a local reusable workflow (`uses: ./.github/workflows/x.yml`)
|
|
944
|
-
passes its own trigger and `if:` down to the called workflow's steps.
|
|
945
|
-
A remote reusable workflow (`uses: owner/repo/...@ref`) is not a
|
|
946
|
-
supported way to wire a gate: it is not read, so a gate must run as a
|
|
947
|
-
`run:` step, such as `bunx checks-comment-gate`, in the repo's own
|
|
948
|
-
workflows.
|
|
949
|
-
|
|
950
|
-
A step running `checks-lint` also counts for a declared gate that calls
|
|
951
|
-
one of the gates `checks-lint` runs by its bare bin name, when the step
|
|
952
|
-
calls `checks-lint` the same way: `bunx checks-lint` counts for
|
|
953
|
-
`bunx checks-comment-gate "origin/$BASE_REF" "$HEAD_SHA"`. A step running `bun run lint` counts only for the `bun run lint` gate,
|
|
954
|
-
since the check never reads what a package script runs. So once `lint`
|
|
955
|
-
runs `checks-lint`, the per-gate entries can leave `gates.ci`
|
|
956
|
-
along with the workflows that ran them.
|
|
957
|
-
|
|
958
|
-
The default branch is `main`; a repo with another one sets
|
|
959
|
-
`"defaultBranch"` in `quality.json`, which `checks-lint` also reads when
|
|
960
|
-
`origin/HEAD` is not set.
|
|
961
|
-
|
|
962
|
-
It exits 1 naming each gap, with every step that runs the gate and why
|
|
963
|
-
that step does not count:
|
|
964
|
-
|
|
965
|
-
```
|
|
966
|
-
ci-wiring: 1 of 8 gate(s) do not run on pull requests to main:
|
|
967
|
-
bun run lint
|
|
968
|
-
.github/workflows/release.yml job publish step 7: .github/workflows/release.yml does not trigger on pull_request
|
|
969
|
-
```
|
|
970
|
-
|
|
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
|
|
973
|
-
workflow does not parse. Whether a workflow is well formed is
|
|
974
|
-
actionlint's question, not this one's.
|
|
975
|
-
|
|
976
|
-
A command a schedule must run, such as the flake run, goes in
|
|
977
|
-
`gates.scheduled`:
|
|
978
|
-
|
|
979
|
-
```json
|
|
980
|
-
"gates": {
|
|
981
|
-
"ci": ["bun run lint", "bun run typecheck", "bun run test"],
|
|
982
|
-
"scheduled": ["bunx checks-flake --runs 10 --report flake-report.json"]
|
|
983
|
-
}
|
|
984
|
-
```
|
|
985
|
-
|
|
986
|
-
Each counts only as a step of the same plain shape in a workflow whose
|
|
987
|
-
`on` carries `schedule` with at least one `cron`, under the same
|
|
988
|
-
`if: false`, `continue-on-error: true` and `needs` rules as a gate. It
|
|
989
|
-
exits 1 naming each one no schedule runs:
|
|
990
|
-
|
|
991
|
-
```
|
|
992
|
-
ci-wiring: 1 of 1 scheduled command(s) do not run on a schedule:
|
|
993
|
-
bunx checks-flake --runs 10 --report flake-report.json
|
|
994
|
-
.github/workflows/ci.yml job checks step 5: .github/workflows/ci.yml does not trigger on a schedule
|
|
995
|
-
```
|
|
996
|
-
|
|
997
|
-
### Gate selection
|
|
998
|
-
|
|
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
|
|
1002
|
-
missing `bun test` script and `bunfig.toml`. It declares the gates
|
|
1003
|
-
`checks-lint` runs as `gates.lint`:
|
|
1004
|
-
|
|
1005
|
-
```json
|
|
1006
|
-
"gates": {
|
|
1007
|
-
"ci": ["bun run lint"],
|
|
1008
|
-
"lint": [
|
|
1009
|
-
"checks-commit-identity",
|
|
1010
|
-
"checks-comment-gate",
|
|
1011
|
-
"checks-suppressions-ratchet",
|
|
1012
|
-
"checks-ci-wiring",
|
|
1013
|
-
"checks-quality"
|
|
1014
|
-
]
|
|
1015
|
-
}
|
|
1016
|
-
```
|
|
1017
|
-
|
|
1018
|
-
`checks-lint` runs exactly those, in the "Lint entry point" table's
|
|
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`
|
|
1023
|
-
keeps.
|
|
1024
|
-
|
|
1025
|
-
A selection may leave out only a gate that does not apply:
|
|
1026
|
-
|
|
1027
|
-
| Gate | Applies when the repository |
|
|
1028
|
-
| --- | --- |
|
|
1029
|
-
| `checks-lint-coverage` | tracks a `.ts` or `.tsx` file |
|
|
1030
|
-
| `checks-test-layout` | tracks a `.ts` or `.tsx` file |
|
|
1031
|
-
| `checks-commit-identity` | always |
|
|
1032
|
-
| `checks-comment-gate` | always |
|
|
1033
|
-
| `checks-suppressions-ratchet` | always |
|
|
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 |
|
|
1038
|
-
|
|
1039
|
-
Both bins exit 2 on a `gates.lint` that names an unknown gate or leaves
|
|
1040
|
-
out one that always applies. ci-wiring exits 1 when the
|
|
1041
|
-
selection leaves out a gate the repository's tracked files make
|
|
1042
|
-
applicable, and names the gate and the files:
|
|
1043
|
-
|
|
1044
|
-
```
|
|
1045
|
-
ci-wiring: quality.json gates.lint leaves out 4 gate(s) this repository's contents make applicable:
|
|
1046
|
-
checks-lint-coverage: the repository tracks TypeScript source (src/widget.ts)
|
|
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)
|
|
1050
|
-
```
|
|
1051
|
-
|
|
1052
|
-
It reads the files tracked at the checkout, so the pull request that
|
|
1053
|
-
adds the first TypeScript file is the one refused.
|
|
1054
|
-
|
|
1055
|
-
### Limits
|
|
1056
|
-
|
|
1057
|
-
The check reads workflow files and never runs them, so it deliberately
|
|
1058
|
-
does not evaluate:
|
|
1059
|
-
|
|
1060
|
-
- an `if:` expression other than a constant `true` or `false`, which
|
|
1061
|
-
counts as running;
|
|
1062
|
-
- a `strategy.matrix` `include` or `exclude`, so a matrix that drops
|
|
1063
|
-
every combination still counts as running its steps;
|
|
1064
|
-
- a remote reusable workflow (`uses: owner/repo/...@ref`), whose steps
|
|
1065
|
-
are never read;
|
|
1066
|
-
- anything that happens at run time on the runner: what the gate
|
|
1067
|
-
command itself does, the shell's options, and a step or job that
|
|
1068
|
-
fails or times out before the gate step.
|
|
1069
|
-
|
|
1070
|
-
## Backtest
|
|
1071
|
-
|
|
1072
|
-
`scripts/backtest.ts` reports what the comment check would have refused
|
|
1073
|
-
at each recent commit, so a repository can measure its own history:
|
|
1074
|
-
|
|
1075
|
-
```sh
|
|
1076
|
-
bun run checks-backtest [commit-count]
|
|
1077
|
-
```
|
|
1078
|
-
|
|
1079
|
-
It walks the last `commit-count` first-parent commits, 60 by default,
|
|
1080
|
-
prints a row per commit that touches code and then the totals,
|
|
1081
|
-
attributes only the refusals each commit introduced, and counts new
|
|
1082
|
-
comment text as a share of added lines.
|
|
1083
|
-
`generated/`, `vendor/`, `repos/`, `node_modules/` and `dist/` are out
|
|
1084
|
-
of reach, so the figures are authored code.
|
|
1085
|
-
|
|
1086
|
-
## Mutation compare
|
|
1087
|
-
|
|
1088
|
-
`checks-mutation-compare` gates a pull request on no-regression rather
|
|
1089
|
-
than an absolute threshold: the head mutation score may not fall below
|
|
1090
|
-
the score at the merge-base.
|
|
1091
|
-
|
|
1092
|
-
```sh
|
|
1093
|
-
checks-mutation-compare <base-report> <head-report> [--advisory]
|
|
1094
|
-
```
|
|
1095
|
-
|
|
1096
|
-
Both reports are Stryker `mutation.json` files. It prints the overall
|
|
1097
|
-
score of each report and the per-file scores that differ, and exits 1
|
|
1098
|
-
when the head score is below the base score. The score is Stryker's:
|
|
1099
|
-
`Killed` and `Timeout` over those plus `Survived` and `NoCoverage`, so
|
|
1100
|
-
`CompileError`, `RuntimeError`, `Ignored` and `Pending` mutants leave
|
|
1101
|
-
it. Every file in a report counts toward that report's score, including
|
|
1102
|
-
files present in only one of the two.
|
|
1103
|
-
|
|
1104
|
-
`--advisory` prints the same verdict and always exits 0. That is how
|
|
1105
|
-
consumers run it for the first month; after that they drop the flag and
|
|
1106
|
-
it blocks.
|
|
1107
|
-
|
|
1108
|
-
Enforcement runs on pull requests, comparing the head report against a
|
|
1109
|
-
report built at the merge-base:
|
|
1110
|
-
|
|
1111
|
-
```yaml
|
|
1112
|
-
jobs:
|
|
1113
|
-
mutation-compare:
|
|
1114
|
-
runs-on: ubuntu-latest
|
|
1115
|
-
steps:
|
|
1116
|
-
- uses: actions/checkout@v5
|
|
1117
|
-
with:
|
|
1118
|
-
fetch-depth: 0
|
|
1119
|
-
- uses: oven-sh/setup-bun@v2
|
|
1120
|
-
- run: bun install --frozen-lockfile
|
|
1121
|
-
- run: bunx stryker run
|
|
1122
|
-
- run: |
|
|
1123
|
-
base="$(git merge-base HEAD origin/main)"
|
|
1124
|
-
git worktree add /tmp/mutation-base "$base"
|
|
1125
|
-
(cd /tmp/mutation-base && bun install --frozen-lockfile && bunx stryker run)
|
|
1126
|
-
- run: bun run checks-mutation-compare --advisory /tmp/mutation-base/reports/mutation/mutation.json reports/mutation/mutation.json
|
|
1127
|
-
```
|
|
1128
|
-
|
|
1129
|
-
The shared Stryker preset's `json` reporter writes
|
|
1130
|
-
`reports/mutation/mutation.json` in each worktree.
|
|
1131
|
-
|
|
1132
|
-
## Why it is shaped this way
|
|
1133
|
-
|
|
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.
|
|
1140
|
-
- `node_modules/` is excluded through the consumer's `.gitignore`, not
|
|
1141
|
-
`ignorePatterns`: oxlint still walks the installed package when only
|
|
1142
|
-
`ignorePatterns` names it.
|
|
1143
|
-
- `files` in package.json is the published surface: `tests/`, `AGENTS.md`
|
|
1144
|
-
and the `.ts` plugin source never reach an install. npm adds
|
|
1145
|
-
`package.json`, `README` and `LICENSE` to the tarball whatever `files`
|
|
1146
|
-
says. `bun pm pack` builds the same tarball the registry serves, which
|
|
1147
|
-
is what the packed-tarball consumer e2e test installs.
|
|
1148
|
-
- The plugin ships compiled as `dist/index.js`, built with
|
|
1149
|
-
`bun build effect-channel/index.ts --outdir dist --target node --format esm`.
|
|
1150
|
-
Node refuses to type-strip a `.ts` plugin under `node_modules`, so the
|
|
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.
|
|
1162
|
-
- `dist/` is committed. No `prepack` or `prepublishOnly` builds it, so a
|
|
1163
|
-
publish ships whatever bundle the publishing worktree holds. Rebuild it
|
|
1164
|
-
after pulling with `bun run build`; CI fails when the committed bundle
|
|
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.
|
|
1178
|
-
- The base parses with swc because typescript 7 (tsgo) has no compiler
|
|
1179
|
-
API for dependency-cruiser to use. Without `@swc/core` installed the
|
|
1180
|
-
cruise silently skips every `.ts` file, so this repo's test asserts its
|
|
1181
|
-
own TypeScript is cruised.
|
|
1182
|
-
- `bunfig.toml` has no `extends` and no include: bun ignores an unknown
|
|
1183
|
-
top-level key in silence, so a preset cannot be inherited and the
|
|
1184
|
-
consumer's copy is compared key by key against the installed one
|
|
1185
|
-
instead. `[test] pathIgnorePatterns` is a real bunfig key, and an empty
|
|
1186
|
-
`--path-ignore-patterns` flag overrides the file's own list.
|
|
1187
|
-
- The Stryker preset is a JavaScript module, not JSON: Stryker 10 does
|
|
1188
|
-
not resolve `extends` in a JSON config, but a `.mjs` config that
|
|
1189
|
-
spreads an imported object consumes it. Keys the consumer sets after
|
|
1190
|
-
the spread win.
|
|
1191
|
-
- The `.ts` bins are written in Effect, so `effect` is a peer dependency
|
|
1192
|
-
and `@effect/platform-bun`, which only the bins use, is a dependency.
|
|
1193
|
-
`@effect/platform-node-shared` is a direct dependency at the same exact
|
|
1194
|
-
version only to pin it: `@effect/platform-bun` asks for it with a `^`
|
|
1195
|
-
range, and a newer rc peers on a newer `effect` than consumers install,
|
|
1196
|
-
so all three move together.
|
|
1197
|
-
- Each runnable script ships a `checks-` bin entry, so consumer
|
|
1198
|
-
`package.json` scripts call the short name, which the package manager
|
|
1199
|
-
puts on `PATH` only there; a shell runs it through `bun run`, which
|
|
1200
|
-
never falls back to the registry the way `bunx` does. The `.ts` checks
|
|
1201
|
-
keep a `bun` shebang, which needs no build step and no `dist/`
|
|
1202
|
-
entry, unlike the oxlint plugin that node loads.
|
|
1203
|
-
- `checks-lint` runs each gate as its own bin in a child process rather
|
|
1204
|
-
than importing it, so a gate behaves the same called alone or through
|
|
1205
|
-
the entry point, and `lint-coverage.sh` stays a shell script. The
|
|
1206
|
-
gates run one at a time with their output passed straight through, so
|
|
1207
|
-
each report reads whole and in the table's order.
|
|
1208
|
-
- A gate selection is checked against the repository's contents rather
|
|
1209
|
-
than trusted, so it cannot skip a gate that applies. ci-wiring does
|
|
1210
|
-
that check, which is why a selection without it, or without another
|
|
1211
|
-
gate that applies everywhere, is refused as `checks-lint` reads it:
|
|
1212
|
-
nothing would check the selection otherwise.
|
|
1213
|
-
- `checks-test` runs bun itself rather than reading a report some other
|
|
1214
|
-
run left: a skip taken only on CI is visible only in CI's own run, and
|
|
1215
|
-
an earlier run's report may be stale or narrowed. It reads the JUnit
|
|
1216
|
-
report bun writes to a temporary directory, since bun has no other
|
|
1217
|
-
per-test output meant for a program.
|
|
1218
|
-
- `checks-ci-wiring` runs inside `lint`, not in a workflow of its own:
|
|
1219
|
-
deleting the step that runs a check is the violation it catches, so the
|
|
1220
|
-
local `lint` is where it has to fail.
|
|
1221
|
-
- Workflows are parsed with `Bun.YAML`, which the `bun` shebang already
|
|
1222
|
-
provides, so the check adds no dependency. It reads `on` as a string
|
|
1223
|
-
key, not as the YAML 1.1 boolean.
|
|
1224
|
-
- `bun` counts as a built-in module. Nothing installed resolves it except
|
|
1225
|
-
`@types/bun`, which would otherwise make every runtime `bun` import look
|
|
1226
|
-
like a dev-only dependency.
|
|
1227
|
-
- The pull request merge commit GitHub builds is authored by `GitHub
|
|
1228
|
-
<noreply@github.com>`, which commit-identity refuses as an author.
|
|
1229
|
-
`checks-lint` ends a pull request's range at the event's head sha, so
|
|
1230
|
-
the merge commit is never in it. A `lint` that calls
|
|
1231
|
-
`checks-commit-identity HEAD` itself checks out
|
|
1232
|
-
`github.event.pull_request.head.sha` instead of the default merge ref.
|
|
1233
|
-
- `no-deep-imports` judges the import specifier, never the resolved file.
|
|
1234
|
-
The base honours `exports` maps, so a subpath the map publishes resolves
|
|
1235
|
-
and passes, one it omits fails to resolve and is reported, and a package
|
|
1236
|
-
without an `exports` map publishes every file. A bare import always
|
|
1237
|
-
passes whatever file its entry lives in. Setting your own
|
|
1238
|
-
`options.enhancedResolveOptions` replaces the base's, so restate
|
|
1239
|
-
`exportsFields` and `conditionNames` if you do.
|
|
1240
|
-
- dependency-cruiser `extends` merges same-name `forbidden` rules with the
|
|
1241
|
-
child's fields winning. That is the entry-point and layer recipe above.
|
|
1242
|
-
|
|
1243
|
-
## Develop
|
|
1244
|
-
|
|
1245
|
-
```sh
|
|
1246
|
-
bun install
|
|
1247
|
-
bun run build
|
|
1248
|
-
bun run lint
|
|
1249
|
-
bun run typecheck
|
|
1250
|
-
bun run test
|
|
1251
|
-
```
|
|
1252
|
-
|
|
1253
|
-
Release by tagging a commit on `main`. The `release` workflow refuses
|
|
1254
|
-
a tag off `main` and reruns the build, `dist/` check, lint, typecheck
|
|
1255
|
-
and tests before it publishes:
|
|
1256
|
-
|
|
1257
|
-
```sh
|
|
1258
|
-
tag="v$(bun -p 'require("./package.json").version')"
|
|
1259
|
-
git tag "$tag" && git push origin "$tag"
|
|
1260
|
-
```
|
|
1261
|
-
|
|
1262
|
-
The `release` workflow publishes the tagged version through npm
|
|
1263
|
-
trusted publishing, so the tag and `package.json` must agree. No
|
|
1264
|
-
token is stored anywhere: GitHub mints the publish credential for
|
|
1265
|
-
each run.
|
|
1266
|
-
|
|
1267
|
-
npm attaches a trusted publisher only to a package that already
|
|
1268
|
-
exists, so the first version goes out by hand: `npm publish` from the
|
|
1269
|
-
tagged commit as `avi2dg`, then add the trusted publisher (repository
|
|
1270
|
-
`avi2d/checks`, workflow `release.yml`) in the package's npm settings.
|
|
1271
|
-
That first tag's `release` run fails on the already-published version;
|
|
1272
|
-
every later tag publishes through the workflow.
|
|
1273
|
-
|
|
1274
|
-
`publishConfig.access` in package.json is what makes the scoped package
|
|
1275
|
-
public.
|
|
158
|
+
| `CHANGELOG.md` | every release, and what it changed |
|
|
159
|
+
| `CONTRIBUTING.md` | how this repository is developed and released |
|
|
160
|
+
| `docs/` | a reference page per bin and per shared config, and why the kit is shaped this way |
|
|
161
|
+
| `bunfig.toml` | the bunfig preset a repository copies |
|
|
162
|
+
| `commitlint.config.js` | the shared commitlint config |
|
|
163
|
+
| `dependency-cruiser.config.js` | the shared dependency-cruiser base |
|
|
164
|
+
| `scripts/` | every bin, which a package script calls by its `checks-` name |
|
|
165
|
+
| `templates/` | one template per kind of doc file, which a new doc file starts from |
|
|
166
|
+
| `presets/` | the Effect rule blocks `checks-quality generate` writes into the fragments |
|
|
167
|
+
| `quality.schema.json` | the schema of `quality.json`, which its `$schema` line names |
|
|
168
|
+
| `oxlintrc.json` | the oxlint base config `.oxlintrc.json` extends |
|
|
169
|
+
| `stryker.preset.js` | the Stryker mutation-testing preset |
|
|
170
|
+
| `tsconfig.effect.json` | the tsconfig fragment with the Effect language-service block |
|
|
171
|
+
| `dist/` | the compiled Effect error-channel plugin and `featureRules` |
|
|
172
|
+
|
|
173
|
+
<!-- end generated shipped -->
|
|
174
|
+
|
|
175
|
+
## Related topics
|
|
176
|
+
|
|
177
|
+
- [The quality file](docs/configs/quality-file.md)
|
|
178
|
+
- [The Effect rules](docs/configs/effect-rules.md)
|
|
179
|
+
- [The dependency rules](docs/configs/dependency-rules.md)
|
|
180
|
+
- [The commit message lint](docs/configs/commit-messages.md)
|
|
181
|
+
- [Why it is shaped this way](docs/design.md)
|
|
182
|
+
- [Contribute to checks](CONTRIBUTING.md)
|