@avi2dg/checks 0.5.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +193 -67
- package/package.json +16 -6
- package/scripts/backtest.ts +23 -33
- package/scripts/ci-wiring.ts +86 -65
- package/scripts/comment-gate.ts +23 -38
- package/scripts/comments.ts +24 -13
- package/scripts/commit-identity.ts +91 -87
- package/scripts/gates.ts +21 -0
- package/scripts/git.ts +27 -0
- package/scripts/lint.ts +162 -0
- package/scripts/main.ts +28 -0
- package/scripts/mutation-compare.ts +43 -57
- package/scripts/suppressions-ratchet.ts +125 -0
- package/scripts/test-layout.ts +67 -44
package/README.md
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# checks
|
|
2
2
|
|
|
3
3
|
Deterministic checks shared across my TypeScript repos. One package,
|
|
4
|
-
`@avi2dg/checks`: the
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
`@avi2dg/checks`: the `checks-lint` entry point that runs every lint gate
|
|
5
|
+
below over a range it resolves itself, the oxlint base config, the
|
|
6
|
+
tsconfig fragment with the Effect language-service block, the shared
|
|
7
|
+
commitlint config, the shared dependency-cruiser base, the test-layout
|
|
8
|
+
check with its bunfig preset, the commit-identity check, the comment
|
|
9
|
+
gate with its backtest, the oxlint suppressions ratchet, the Stryker
|
|
10
|
+
mutation-testing preset with its no-regression comparator, the CI-wiring
|
|
11
|
+
check, and the Effect error-channel plugin compiled to JavaScript.
|
|
11
12
|
|
|
12
13
|
Published as `@avi2dg/checks` on the public npm registry.
|
|
13
14
|
|
|
@@ -16,7 +17,7 @@ Published as `@avi2dg/checks` on the public npm registry.
|
|
|
16
17
|
From the consuming repo:
|
|
17
18
|
|
|
18
19
|
```sh
|
|
19
|
-
bun add -d @avi2dg/checks 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
|
|
20
|
+
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
|
|
20
21
|
```
|
|
21
22
|
|
|
22
23
|
`.oxlintrc.json`:
|
|
@@ -51,11 +52,14 @@ cp node_modules/@avi2dg/checks/bunfig.toml bunfig.toml
|
|
|
51
52
|
`package.json` gains three scripts:
|
|
52
53
|
|
|
53
54
|
```json
|
|
54
|
-
"lint": "oxlint --type-aware && checks-lint
|
|
55
|
+
"lint": "oxlint --type-aware && checks-lint",
|
|
55
56
|
"typecheck": "tsc --noEmit && effect-tsgo diagnostics --project tsconfig.json --format text --strict",
|
|
56
57
|
"test": "bun test --randomize"
|
|
57
58
|
```
|
|
58
59
|
|
|
60
|
+
`checks-lint` runs every kit gate a lint needs; see "Lint entry point"
|
|
61
|
+
below. Three of them:
|
|
62
|
+
|
|
59
63
|
`lint-coverage.sh` fails when oxlint silently skips a tracked `.ts` or
|
|
60
64
|
`.tsx` file, for example through a stray `.gitignore` entry. It compares
|
|
61
65
|
`git ls-files` against oxlint's own file walk and names the missing files.
|
|
@@ -80,6 +84,81 @@ export default {
|
|
|
80
84
|
The registry version is pinned by the consumer's lockfile; bump
|
|
81
85
|
`@avi2dg/checks` to adopt a new release.
|
|
82
86
|
|
|
87
|
+
## Lint entry point
|
|
88
|
+
|
|
89
|
+
`checks-lint` runs each of the kit's lint gates in turn and names every
|
|
90
|
+
one that fails, rather than stopping at the first:
|
|
91
|
+
|
|
92
|
+
| Gate | Reads |
|
|
93
|
+
| --- | --- |
|
|
94
|
+
| `checks-lint-coverage` | the working tree |
|
|
95
|
+
| `checks-test-layout` | the working tree |
|
|
96
|
+
| `checks-commit-identity` | the range |
|
|
97
|
+
| `checks-comment-gate` | the range |
|
|
98
|
+
| `checks-suppressions-ratchet` | the range |
|
|
99
|
+
| `checks-ci-wiring` | the working tree |
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
checks-lint
|
|
103
|
+
checks-lint <base-ref> <head-ref>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
It resolves the range once and hands the same one to every range gate.
|
|
107
|
+
Locally, and on any event other than a pull request, the range ends at
|
|
108
|
+
`HEAD` and starts where `HEAD` branched from the origin default branch:
|
|
109
|
+
`origin/HEAD`, or when `origin/HEAD` is not set, as in an
|
|
110
|
+
`actions/checkout` clone, `origin/<ciWiring.defaultBranch>` from the
|
|
111
|
+
repository's `package.json` (see "CI wiring"), and `origin/main` when
|
|
112
|
+
that is not declared. In a GitHub Actions pull request, where
|
|
113
|
+
`GITHUB_EVENT_NAME` is `pull_request`, it ends
|
|
114
|
+
at the event's head sha and starts where that branched from
|
|
115
|
+
`origin/<base branch>`, so GitHub's merge commit is never in it. The
|
|
116
|
+
base branch is read from the fetch, not from the event's recorded base
|
|
117
|
+
sha, which GitHub leaves stale once the base branch advances after the
|
|
118
|
+
pull request opens. Explicit base and head arguments override both.
|
|
119
|
+
|
|
120
|
+
The range always starts at the merge base, never at the base branch's
|
|
121
|
+
tip: commits the base branch gained after the head branched off would
|
|
122
|
+
otherwise count against the head. When the head is the merge base, as
|
|
123
|
+
on a push to the default branch or a local run on it, the range would be
|
|
124
|
+
empty, so each range gate is handed that tip commit alone and checks it
|
|
125
|
+
against its parent:
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
checks-lint: tip 10ba7d8935b73ed72624120a1542e51bd21ca7c7 from HEAD against origin/main
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
It prints the range, each gate's own report, then its verdict:
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
checks-lint: range 2504acf098d120e73a8ece3c96f22b934f35c6a8..10ba7d8935b73ed72624120a1542e51bd21ca7c7 from HEAD against origin/main
|
|
135
|
+
...
|
|
136
|
+
checks-lint: 3 of 6 gate(s) failed: checks-commit-identity, checks-comment-gate, checks-suppressions-ratchet
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
It exits 1 when any gate found a violation, and 2 when the range does
|
|
140
|
+
not resolve or no failing gate could decide. Every gate runs, so a
|
|
141
|
+
repository on `checks-lint` declares `ciWiring.gates` (see "CI wiring")
|
|
142
|
+
and holds the test layout.
|
|
143
|
+
|
|
144
|
+
CI runs it through `lint`. The checkout fetches the whole history, which
|
|
145
|
+
the merge base needs:
|
|
146
|
+
|
|
147
|
+
```yaml
|
|
148
|
+
on:
|
|
149
|
+
pull_request:
|
|
150
|
+
jobs:
|
|
151
|
+
lint:
|
|
152
|
+
runs-on: ubuntu-latest
|
|
153
|
+
steps:
|
|
154
|
+
- uses: actions/checkout@v5
|
|
155
|
+
with:
|
|
156
|
+
fetch-depth: 0
|
|
157
|
+
- uses: oven-sh/setup-bun@v2
|
|
158
|
+
- run: bun install --frozen-lockfile
|
|
159
|
+
- run: bun run lint
|
|
160
|
+
```
|
|
161
|
+
|
|
83
162
|
## Effect rules
|
|
84
163
|
|
|
85
164
|
The base config loads the `effect-channel` plugin and turns on
|
|
@@ -122,6 +201,45 @@ oxlint resolves `files` against the directory of the config that holds
|
|
|
122
201
|
the override, so a config passed with `-c` from outside the repository
|
|
123
202
|
matches nothing and reports nothing.
|
|
124
203
|
|
|
204
|
+
This repository's own override for `scripts/**` adds `node/no-sync`,
|
|
205
|
+
`oxc/no-async-await`, `promise/avoid-new` and `unicorn/no-process-exit`,
|
|
206
|
+
which need the `node`, `promise` and `unicorn` plugins in the override's
|
|
207
|
+
`plugins`. `unicorn/no-process-exit` passes over any file that opens with
|
|
208
|
+
a shebang, so a bin also needs `no-restricted-properties` on
|
|
209
|
+
`process.exit`. Sites standing when the override lands go in oxlint's own
|
|
210
|
+
baseline, `oxlint --suppress-all`, so their count can only fall.
|
|
211
|
+
|
|
212
|
+
The language service holds the same paths to Effect-native IO through
|
|
213
|
+
`overrides` in the plugin block of `tsconfig.json`. effect-tsgo keeps the
|
|
214
|
+
severities `tsconfig.effect.json` sets when the child config restates the
|
|
215
|
+
plugin with only its overrides:
|
|
216
|
+
|
|
217
|
+
```json
|
|
218
|
+
{
|
|
219
|
+
"extends": "@avi2dg/checks/tsconfig.effect.json",
|
|
220
|
+
"compilerOptions": {
|
|
221
|
+
"plugins": [
|
|
222
|
+
{
|
|
223
|
+
"name": "@effect/language-service",
|
|
224
|
+
"overrides": [
|
|
225
|
+
{
|
|
226
|
+
"include": ["src/**/*.ts"],
|
|
227
|
+
"options": {
|
|
228
|
+
"diagnosticSeverity": {
|
|
229
|
+
"nodeBuiltinImport": "error",
|
|
230
|
+
"asyncFunction": "error",
|
|
231
|
+
"newPromise": "error",
|
|
232
|
+
"extendsNativeError": "error"
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
]
|
|
237
|
+
}
|
|
238
|
+
]
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
125
243
|
## Test layout
|
|
126
244
|
|
|
127
245
|
`checks-test-layout` fails unless the repo holds this shape, and names the
|
|
@@ -145,7 +263,7 @@ file and the path to move it to when it does not:
|
|
|
145
263
|
with swc and reads import specifiers and identifier use, so a test that
|
|
146
264
|
only carries `"node:child_process"` as a string is not a violation.
|
|
147
265
|
- `scripts.test` is exactly `bun test --randomize` and `scripts.lint` runs
|
|
148
|
-
this check.
|
|
266
|
+
this check, itself or through `checks-lint` called by its bare bin name.
|
|
149
267
|
- `bunfig.toml` carries every `[test]` key of the shipped preset with the
|
|
150
268
|
same value, and `[test].pathIgnorePatterns` is always
|
|
151
269
|
`["**/tests/quarantine/**"]`: the check pins it itself, so this repo,
|
|
@@ -261,11 +379,10 @@ bun run checks-commit-identity <base-ref> <head-ref>
|
|
|
261
379
|
bun run checks-commit-identity <ref>
|
|
262
380
|
```
|
|
263
381
|
|
|
264
|
-
With one argument it checks that commit alone
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
commit and reason. Other trailers and prose mentioning an address in the
|
|
382
|
+
With one argument it checks that commit alone. It refuses a commit
|
|
383
|
+
whose author or committer is outside the allowlist, and one whose
|
|
384
|
+
trailer block carries a `Co-authored-by:` trailer as git parses it, and
|
|
385
|
+
it names the offending commit and reason. Other trailers and prose mentioning an address in the
|
|
269
386
|
body are left alone. `GitHub <noreply@github.com>` is allowed as
|
|
270
387
|
committer only, since that is who writes a squash merge.
|
|
271
388
|
|
|
@@ -278,31 +395,7 @@ owners restates it in `package.json`:
|
|
|
278
395
|
}
|
|
279
396
|
```
|
|
280
397
|
|
|
281
|
-
|
|
282
|
-
current tip to the head is visible:
|
|
283
|
-
|
|
284
|
-
```yaml
|
|
285
|
-
on:
|
|
286
|
-
pull_request:
|
|
287
|
-
types: [opened, edited, synchronize, reopened]
|
|
288
|
-
jobs:
|
|
289
|
-
commit-identity:
|
|
290
|
-
runs-on: ubuntu-latest
|
|
291
|
-
steps:
|
|
292
|
-
- uses: actions/checkout@v5
|
|
293
|
-
with:
|
|
294
|
-
fetch-depth: 0
|
|
295
|
-
- uses: oven-sh/setup-bun@v2
|
|
296
|
-
- run: bun install --frozen-lockfile
|
|
297
|
-
- run: bunx checks-commit-identity "origin/$BASE_REF" "$HEAD_SHA"
|
|
298
|
-
env:
|
|
299
|
-
BASE_REF: ${{ github.event.pull_request.base.ref }}
|
|
300
|
-
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
The checkout fetches the full history and the range starts at the
|
|
304
|
-
fetched base branch, not the event's recorded base sha, which GitHub
|
|
305
|
-
leaves stale once the base branch advances after the pull request opens.
|
|
398
|
+
`checks-lint` runs it over each pull request's range; see "Lint entry point".
|
|
306
399
|
|
|
307
400
|
## Comment gate
|
|
308
401
|
|
|
@@ -325,28 +418,40 @@ pointer, a doc block, and a file opening with a rationale block over
|
|
|
325
418
|
three lines, licence headers excepted; `scripts/comments.ts` holds the
|
|
326
419
|
scanner the gate and the backtest share.
|
|
327
420
|
|
|
328
|
-
|
|
329
|
-
current tip to the head is visible:
|
|
421
|
+
`checks-lint` runs it over each pull request's range; see "Lint entry point".
|
|
330
422
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
- run: bun install --frozen-lockfile
|
|
344
|
-
- run: bunx checks-comment-gate "origin/$BASE_REF" "$HEAD_SHA"
|
|
345
|
-
env:
|
|
346
|
-
BASE_REF: ${{ github.event.pull_request.base.ref }}
|
|
347
|
-
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
423
|
+
## Suppressions ratchet
|
|
424
|
+
|
|
425
|
+
`scripts/suppressions-ratchet.ts` holds oxlint's bulk-suppression
|
|
426
|
+
baseline, `oxlint-suppressions.json`, to counts that only fall. oxlint
|
|
427
|
+
accepts whatever `oxlint --suppress-all` writes, so raising a count to
|
|
428
|
+
let a new site through passes the lint. The ratchet reads the file at a
|
|
429
|
+
base and a head and fails naming each file and rule whose count rose or
|
|
430
|
+
that appeared:
|
|
431
|
+
|
|
432
|
+
```sh
|
|
433
|
+
bun run checks-suppressions-ratchet <base-ref> <head-ref>
|
|
434
|
+
bun run checks-suppressions-ratchet <ref>
|
|
348
435
|
```
|
|
349
436
|
|
|
437
|
+
With one argument it compares that commit with its parent, and exits 2
|
|
438
|
+
when the parent is not in the clone. With two it reads the base where
|
|
439
|
+
the head branched off, at their merge-base, so a count the base branch
|
|
440
|
+
lowered since does not read as a rise at the head. A count that fell and
|
|
441
|
+
an entry that went both pass. A commit without the file counts as empty,
|
|
442
|
+
so the commit that first adds a baseline fails with every entry
|
|
443
|
+
appearing. The file is read from the directory the command runs in,
|
|
444
|
+
which is where oxlint writes it. It exits 2 when a ref does not resolve
|
|
445
|
+
or the file is not oxlint's count per rule per file:
|
|
446
|
+
|
|
447
|
+
```
|
|
448
|
+
suppressions-ratchet: 2 count(s) in oxlint-suppressions.json rose or appeared; fix the site instead of suppressing it:
|
|
449
|
+
src/added.ts typescript/no-unsafe-type-assertion appeared with 1
|
|
450
|
+
src/dispatch.ts typescript/no-non-null-assertion rose from 12 to 13
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
`checks-lint` runs it over each pull request's range; see "Lint entry point".
|
|
454
|
+
|
|
350
455
|
## CI wiring
|
|
351
456
|
|
|
352
457
|
`checks-ci-wiring` fails when a command the repository's CI must run no
|
|
@@ -355,14 +460,14 @@ that: a workflow whose lint step became a no-op leaves `bun run lint`
|
|
|
355
460
|
green.
|
|
356
461
|
|
|
357
462
|
The repository declares its gates once, in `package.json`, and `lint`
|
|
358
|
-
runs the check
|
|
463
|
+
runs the check through `checks-lint`:
|
|
359
464
|
|
|
360
465
|
```json
|
|
361
466
|
"ciWiring": {
|
|
362
467
|
"gates": ["bun run lint", "bun run typecheck", "bun run test"]
|
|
363
468
|
},
|
|
364
469
|
"scripts": {
|
|
365
|
-
"lint": "oxlint --type-aware && checks-lint
|
|
470
|
+
"lint": "oxlint --type-aware && checks-lint"
|
|
366
471
|
}
|
|
367
472
|
```
|
|
368
473
|
|
|
@@ -397,8 +502,17 @@ supported way to wire a gate: it is not read, so a gate must run as a
|
|
|
397
502
|
`run:` step, such as `bunx checks-comment-gate`, in the repo's own
|
|
398
503
|
workflows.
|
|
399
504
|
|
|
505
|
+
A step running `checks-lint` also counts for a declared gate that calls
|
|
506
|
+
one of the gates `checks-lint` runs by its bare bin name, when the step
|
|
507
|
+
calls `checks-lint` the same way: `bunx checks-lint` counts for
|
|
508
|
+
`bunx checks-comment-gate "origin/$BASE_REF" "$HEAD_SHA"`. A step running `bun run lint` counts only for the `bun run lint` gate,
|
|
509
|
+
since the check never reads what a package script runs. So once `lint`
|
|
510
|
+
runs `checks-lint`, the per-gate entries can leave `ciWiring.gates`
|
|
511
|
+
along with the workflows that ran them.
|
|
512
|
+
|
|
400
513
|
The default branch is `main`; a repo with another one sets
|
|
401
|
-
`"defaultBranch"` beside `"gates"
|
|
514
|
+
`"defaultBranch"` beside `"gates"`, which `checks-lint` also reads when
|
|
515
|
+
`origin/HEAD` is not set.
|
|
402
516
|
|
|
403
517
|
It exits 1 naming each gap, with every step that runs the gate and why
|
|
404
518
|
that step does not count:
|
|
@@ -524,12 +638,23 @@ The shared Stryker preset's `json` reporter writes
|
|
|
524
638
|
not resolve `extends` in a JSON config, but a `.mjs` config that
|
|
525
639
|
spreads an imported object consumes it. Keys the consumer sets after
|
|
526
640
|
the spread win.
|
|
641
|
+
- The `.ts` bins are written in Effect, so `effect` is a peer dependency
|
|
642
|
+
and `@effect/platform-bun`, which only the bins use, is a dependency.
|
|
643
|
+
`@effect/platform-node-shared` is a direct dependency at the same exact
|
|
644
|
+
version only to pin it: `@effect/platform-bun` asks for it with a `^`
|
|
645
|
+
range, and a newer rc peers on a newer `effect` than consumers install,
|
|
646
|
+
so all three move together.
|
|
527
647
|
- Each runnable script ships a `checks-` bin entry, so consumer
|
|
528
648
|
`package.json` scripts call the short name, which the package manager
|
|
529
649
|
puts on `PATH` only there; a shell runs it through `bun run`, which
|
|
530
650
|
never falls back to the registry the way `bunx` does. The `.ts` checks
|
|
531
651
|
keep a `bun` shebang, which needs no build step and no `dist/`
|
|
532
652
|
entry, unlike the oxlint plugin that node loads.
|
|
653
|
+
- `checks-lint` runs each gate as its own bin in a child process rather
|
|
654
|
+
than importing it, so a gate behaves the same called alone or through
|
|
655
|
+
the entry point, and `lint-coverage.sh` stays a shell script. The
|
|
656
|
+
gates run one at a time with their output passed straight through, so
|
|
657
|
+
each report reads whole and in the table's order.
|
|
533
658
|
- `checks-ci-wiring` runs inside `lint`, not in a workflow of its own:
|
|
534
659
|
deleting the step that runs a check is the violation it catches, so the
|
|
535
660
|
local `lint` is where it has to fail.
|
|
@@ -540,10 +665,11 @@ The shared Stryker preset's `json` reporter writes
|
|
|
540
665
|
`@types/bun`, which would otherwise make every runtime `bun` import look
|
|
541
666
|
like a dev-only dependency.
|
|
542
667
|
- The pull request merge commit GitHub builds is authored by `GitHub
|
|
543
|
-
<noreply@github.com>`, which commit-identity refuses as an author.
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
668
|
+
<noreply@github.com>`, which commit-identity refuses as an author.
|
|
669
|
+
`checks-lint` ends a pull request's range at the event's head sha, so
|
|
670
|
+
the merge commit is never in it. A `lint` that calls
|
|
671
|
+
`checks-commit-identity HEAD` itself checks out
|
|
672
|
+
`github.event.pull_request.head.sha` instead of the default merge ref.
|
|
547
673
|
- `no-deep-imports` judges the import specifier, never the resolved file.
|
|
548
674
|
The base honours `exports` maps, so a subpath the map publishes resolves
|
|
549
675
|
and passes, one it omits fails to resolve and is reported, and a package
|
|
@@ -569,7 +695,7 @@ a tag off `main` and reruns the build, `dist/` check, lint, typecheck
|
|
|
569
695
|
and tests before it publishes:
|
|
570
696
|
|
|
571
697
|
```sh
|
|
572
|
-
git tag v0.
|
|
698
|
+
git tag v0.8.0 && git push origin v0.8.0
|
|
573
699
|
```
|
|
574
700
|
|
|
575
701
|
The `release` workflow publishes the tagged version through npm
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avi2dg/checks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Deterministic checks shared across the captain's TypeScript repos",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -16,12 +16,17 @@
|
|
|
16
16
|
"commitlint.config.js",
|
|
17
17
|
"dependency-cruiser.config.js",
|
|
18
18
|
"scripts/lint-coverage.sh",
|
|
19
|
+
"scripts/main.ts",
|
|
20
|
+
"scripts/git.ts",
|
|
21
|
+
"scripts/gates.ts",
|
|
22
|
+
"scripts/lint.ts",
|
|
19
23
|
"scripts/test-layout.ts",
|
|
20
24
|
"scripts/commit-identity.ts",
|
|
21
25
|
"scripts/mutation-compare.ts",
|
|
22
26
|
"scripts/ci-wiring.ts",
|
|
23
27
|
"scripts/comments.ts",
|
|
24
28
|
"scripts/comment-gate.ts",
|
|
29
|
+
"scripts/suppressions-ratchet.ts",
|
|
25
30
|
"scripts/backtest.ts",
|
|
26
31
|
"oxlintrc.json",
|
|
27
32
|
"stryker.preset.js",
|
|
@@ -33,12 +38,14 @@
|
|
|
33
38
|
"./commitlint.config.js": "./commitlint.config.js",
|
|
34
39
|
"./dependency-cruiser.config.js": "./dependency-cruiser.config.js",
|
|
35
40
|
"./scripts/lint-coverage.sh": "./scripts/lint-coverage.sh",
|
|
41
|
+
"./scripts/lint.ts": "./scripts/lint.ts",
|
|
36
42
|
"./scripts/test-layout.ts": "./scripts/test-layout.ts",
|
|
37
43
|
"./scripts/commit-identity.ts": "./scripts/commit-identity.ts",
|
|
38
44
|
"./scripts/mutation-compare.ts": "./scripts/mutation-compare.ts",
|
|
39
45
|
"./scripts/ci-wiring.ts": "./scripts/ci-wiring.ts",
|
|
40
46
|
"./scripts/comments.ts": "./scripts/comments.ts",
|
|
41
47
|
"./scripts/comment-gate.ts": "./scripts/comment-gate.ts",
|
|
48
|
+
"./scripts/suppressions-ratchet.ts": "./scripts/suppressions-ratchet.ts",
|
|
42
49
|
"./scripts/backtest.ts": "./scripts/backtest.ts",
|
|
43
50
|
"./oxlintrc.json": "./oxlintrc.json",
|
|
44
51
|
"./stryker.preset.js": "./stryker.preset.js",
|
|
@@ -46,17 +53,19 @@
|
|
|
46
53
|
"./dist/index.js": "./dist/index.js"
|
|
47
54
|
},
|
|
48
55
|
"bin": {
|
|
56
|
+
"checks-lint": "scripts/lint.ts",
|
|
49
57
|
"checks-lint-coverage": "scripts/lint-coverage.sh",
|
|
50
58
|
"checks-test-layout": "scripts/test-layout.ts",
|
|
51
59
|
"checks-commit-identity": "scripts/commit-identity.ts",
|
|
52
60
|
"checks-mutation-compare": "scripts/mutation-compare.ts",
|
|
53
61
|
"checks-ci-wiring": "scripts/ci-wiring.ts",
|
|
54
62
|
"checks-comment-gate": "scripts/comment-gate.ts",
|
|
63
|
+
"checks-suppressions-ratchet": "scripts/suppressions-ratchet.ts",
|
|
55
64
|
"checks-backtest": "scripts/backtest.ts"
|
|
56
65
|
},
|
|
57
66
|
"scripts": {
|
|
58
67
|
"build": "bun build effect-channel/index.ts --outdir dist --target node --format esm",
|
|
59
|
-
"lint": "oxlint --type-aware &&
|
|
68
|
+
"lint": "oxlint --type-aware && bun scripts/lint.ts && depcruise --config .dependency-cruiser.cjs effect-channel scripts tests commitlint.config.js dependency-cruiser.config.js stryker.preset.js .dependency-cruiser.cjs",
|
|
60
69
|
"typecheck": "tsc --noEmit && effect-tsgo diagnostics --project tsconfig.json --format text --strict",
|
|
61
70
|
"test": "bun test --randomize"
|
|
62
71
|
},
|
|
@@ -67,15 +76,14 @@
|
|
|
67
76
|
"bun run lint",
|
|
68
77
|
"bun run typecheck",
|
|
69
78
|
"bun run test",
|
|
70
|
-
"./node_modules/.bin/commitlint"
|
|
71
|
-
"bun .checks/scripts/commit-identity.ts",
|
|
72
|
-
"bun .checks/scripts/comment-gate.ts"
|
|
79
|
+
"./node_modules/.bin/commitlint"
|
|
73
80
|
]
|
|
74
81
|
},
|
|
75
82
|
"peerDependencies": {
|
|
76
83
|
"@swc/core": "1.16.2",
|
|
77
84
|
"dependency-cruiser": "18.4.0",
|
|
78
85
|
"@effect/tsgo": "0.45.0",
|
|
86
|
+
"effect": "4.0.0-rc.115",
|
|
79
87
|
"oxlint": "1.83.0",
|
|
80
88
|
"oxlint-tsgolint": "7.0.2002"
|
|
81
89
|
},
|
|
@@ -92,6 +100,8 @@
|
|
|
92
100
|
},
|
|
93
101
|
"dependencies": {
|
|
94
102
|
"@commitlint/cli": "21.2.3",
|
|
95
|
-
"@commitlint/config-conventional": "21.2.3"
|
|
103
|
+
"@commitlint/config-conventional": "21.2.3",
|
|
104
|
+
"@effect/platform-bun": "4.0.0-rc.115",
|
|
105
|
+
"@effect/platform-node-shared": "4.0.0-rc.115"
|
|
96
106
|
}
|
|
97
107
|
}
|
package/scripts/backtest.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
import { Console, Effect, Option } from "effect";
|
|
2
3
|
import { comments, refused, SYNTAXES } from "./comments.ts";
|
|
4
|
+
import { git } from "./git.ts";
|
|
5
|
+
import { runMain, Usage } from "./main.ts";
|
|
3
6
|
|
|
4
7
|
type Landed = {
|
|
5
8
|
readonly sha: string;
|
|
@@ -13,37 +16,21 @@ const USAGE = "usage: backtest.ts [commit-count]";
|
|
|
13
16
|
|
|
14
17
|
const EXCLUDED = ["generated/", "vendor/", "repos/", "node_modules/", "dist/"];
|
|
15
18
|
|
|
16
|
-
function die(message: string): never {
|
|
17
|
-
console.error(message);
|
|
18
|
-
process.exit(2);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function git(...args: readonly string[]): string {
|
|
22
|
-
const result = Bun.spawnSync(["git", ...args], { stdout: "pipe", stderr: "pipe" });
|
|
23
|
-
if (!result.success) {
|
|
24
|
-
die(`backtest: git ${args.join(" ")}: ${result.stderr.toString().trim()}`);
|
|
25
|
-
}
|
|
26
|
-
return result.stdout.toString();
|
|
27
|
-
}
|
|
28
|
-
|
|
29
19
|
function readable(path: string): boolean {
|
|
30
20
|
if (EXCLUDED.some((prefix) => path.startsWith(prefix))) return false;
|
|
31
21
|
const extension = path.slice(path.lastIndexOf(".") + 1);
|
|
32
22
|
return extension in SYNTAXES;
|
|
33
23
|
}
|
|
34
24
|
|
|
35
|
-
|
|
36
|
-
const result = Bun.spawnSync(["git", "show", `${sha}:${path}`], { stdout: "pipe", stderr: "pipe" });
|
|
37
|
-
return result.success ? result.stdout.toString() : undefined;
|
|
38
|
-
}
|
|
25
|
+
const at = (sha: string, path: string) => git(["show", `${sha}:${path}`]).pipe(Effect.option);
|
|
39
26
|
|
|
40
27
|
function withoutLine(refusal: string): string {
|
|
41
28
|
return refusal.replace(/^(\S+?):\d+ /, "$1 ");
|
|
42
29
|
}
|
|
43
30
|
|
|
44
|
-
export
|
|
31
|
+
export const backtest = Effect.fn("backtest")(function* (count: string) {
|
|
45
32
|
const lines: string[] = [];
|
|
46
|
-
const commits = git("log", "--first-parent", "--format=%H%x09%P%x09%s", "-n", count)
|
|
33
|
+
const commits = (yield* git(["log", "--first-parent", "--format=%H%x09%P%x09%s", "-n", count]))
|
|
47
34
|
.trim()
|
|
48
35
|
.split("\n")
|
|
49
36
|
.map((row) => {
|
|
@@ -55,13 +42,13 @@ export async function backtest(count: string): Promise<string> {
|
|
|
55
42
|
const landed: Landed[] = [];
|
|
56
43
|
|
|
57
44
|
for (const { sha, parent, subject } of commits) {
|
|
58
|
-
const changed = git("diff", "--name-only", "--diff-filter=d", parent, sha)
|
|
45
|
+
const changed = (yield* git(["diff", "--name-only", "--diff-filter=d", parent, sha]))
|
|
59
46
|
.trim()
|
|
60
47
|
.split("\n")
|
|
61
48
|
.filter((path) => path !== "" && readable(path));
|
|
62
49
|
if (changed.length === 0) continue;
|
|
63
50
|
|
|
64
|
-
const numstat = git("diff", "--numstat", parent, sha, "--", ...changed).trim();
|
|
51
|
+
const numstat = (yield* git(["diff", "--numstat", parent, sha, "--", ...changed])).trim();
|
|
65
52
|
const added =
|
|
66
53
|
numstat === ""
|
|
67
54
|
? 0
|
|
@@ -71,17 +58,17 @@ export async function backtest(count: string): Promise<string> {
|
|
|
71
58
|
const refusals: string[] = [];
|
|
72
59
|
|
|
73
60
|
for (const path of changed) {
|
|
74
|
-
const after =
|
|
75
|
-
if (after
|
|
76
|
-
const before = (
|
|
61
|
+
const after = yield* at(sha, path);
|
|
62
|
+
if (Option.isNone(after)) continue;
|
|
63
|
+
const before = Option.getOrElse(yield* at(parent, path), () => "");
|
|
77
64
|
|
|
78
|
-
const wasRefused = new Set(refused(path, before).map(withoutLine));
|
|
79
|
-
for (const refusal of refused(path, after)) {
|
|
65
|
+
const wasRefused = new Set((yield* refused(path, before)).map(withoutLine));
|
|
66
|
+
for (const refusal of yield* refused(path, after.value)) {
|
|
80
67
|
if (!wasRefused.has(withoutLine(refusal))) refusals.push(refusal);
|
|
81
68
|
}
|
|
82
69
|
|
|
83
|
-
const wasComment = new Set(comments(path, before).map((one) => one.text));
|
|
84
|
-
for (const one of comments(path, after)) {
|
|
70
|
+
const wasComment = new Set((yield* comments(path, before)).map((one) => one.text));
|
|
71
|
+
for (const one of yield* comments(path, after.value)) {
|
|
85
72
|
if (!wasComment.has(one.text)) commentLines += one.text.split("\n").length;
|
|
86
73
|
}
|
|
87
74
|
}
|
|
@@ -108,10 +95,13 @@ export async function backtest(count: string): Promise<string> {
|
|
|
108
95
|
for (const refusal of one.refusals) lines.push(` ${refusal}`);
|
|
109
96
|
}
|
|
110
97
|
return lines.join("\n");
|
|
111
|
-
}
|
|
98
|
+
});
|
|
112
99
|
|
|
113
|
-
|
|
100
|
+
const main = Effect.gen(function* () {
|
|
114
101
|
const [count = "60", ...extra] = process.argv.slice(2);
|
|
115
|
-
if (extra.length > 0)
|
|
116
|
-
|
|
117
|
-
|
|
102
|
+
if (extra.length > 0) return yield* new Usage({ message: USAGE });
|
|
103
|
+
yield* Console.log(yield* backtest(count));
|
|
104
|
+
return true;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (import.meta.main) runMain("backtest", main);
|