@avi2dg/checks 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +164 -91
- package/package.json +13 -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 +59 -76
- package/scripts/test-layout.ts +67 -44
package/README.md
CHANGED
|
@@ -1,14 +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
|
-
|
|
11
|
-
JavaScript.
|
|
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.
|
|
12
12
|
|
|
13
13
|
Published as `@avi2dg/checks` on the public npm registry.
|
|
14
14
|
|
|
@@ -17,7 +17,7 @@ Published as `@avi2dg/checks` on the public npm registry.
|
|
|
17
17
|
From the consuming repo:
|
|
18
18
|
|
|
19
19
|
```sh
|
|
20
|
-
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
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
`.oxlintrc.json`:
|
|
@@ -52,11 +52,14 @@ cp node_modules/@avi2dg/checks/bunfig.toml bunfig.toml
|
|
|
52
52
|
`package.json` gains three scripts:
|
|
53
53
|
|
|
54
54
|
```json
|
|
55
|
-
"lint": "oxlint --type-aware && checks-lint
|
|
55
|
+
"lint": "oxlint --type-aware && checks-lint",
|
|
56
56
|
"typecheck": "tsc --noEmit && effect-tsgo diagnostics --project tsconfig.json --format text --strict",
|
|
57
57
|
"test": "bun test --randomize"
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
`checks-lint` runs every kit gate a lint needs; see "Lint entry point"
|
|
61
|
+
below. Three of them:
|
|
62
|
+
|
|
60
63
|
`lint-coverage.sh` fails when oxlint silently skips a tracked `.ts` or
|
|
61
64
|
`.tsx` file, for example through a stray `.gitignore` entry. It compares
|
|
62
65
|
`git ls-files` against oxlint's own file walk and names the missing files.
|
|
@@ -81,6 +84,81 @@ export default {
|
|
|
81
84
|
The registry version is pinned by the consumer's lockfile; bump
|
|
82
85
|
`@avi2dg/checks` to adopt a new release.
|
|
83
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
|
+
|
|
84
162
|
## Effect rules
|
|
85
163
|
|
|
86
164
|
The base config loads the `effect-channel` plugin and turns on
|
|
@@ -123,6 +201,45 @@ oxlint resolves `files` against the directory of the config that holds
|
|
|
123
201
|
the override, so a config passed with `-c` from outside the repository
|
|
124
202
|
matches nothing and reports nothing.
|
|
125
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
|
+
|
|
126
243
|
## Test layout
|
|
127
244
|
|
|
128
245
|
`checks-test-layout` fails unless the repo holds this shape, and names the
|
|
@@ -146,7 +263,7 @@ file and the path to move it to when it does not:
|
|
|
146
263
|
with swc and reads import specifiers and identifier use, so a test that
|
|
147
264
|
only carries `"node:child_process"` as a string is not a violation.
|
|
148
265
|
- `scripts.test` is exactly `bun test --randomize` and `scripts.lint` runs
|
|
149
|
-
this check.
|
|
266
|
+
this check, itself or through `checks-lint` called by its bare bin name.
|
|
150
267
|
- `bunfig.toml` carries every `[test]` key of the shipped preset with the
|
|
151
268
|
same value, and `[test].pathIgnorePatterns` is always
|
|
152
269
|
`["**/tests/quarantine/**"]`: the check pins it itself, so this repo,
|
|
@@ -262,11 +379,10 @@ bun run checks-commit-identity <base-ref> <head-ref>
|
|
|
262
379
|
bun run checks-commit-identity <ref>
|
|
263
380
|
```
|
|
264
381
|
|
|
265
|
-
With one argument it checks that commit alone
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
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
|
|
270
386
|
body are left alone. `GitHub <noreply@github.com>` is allowed as
|
|
271
387
|
committer only, since that is who writes a squash merge.
|
|
272
388
|
|
|
@@ -279,31 +395,7 @@ owners restates it in `package.json`:
|
|
|
279
395
|
}
|
|
280
396
|
```
|
|
281
397
|
|
|
282
|
-
|
|
283
|
-
current tip to the head is visible:
|
|
284
|
-
|
|
285
|
-
```yaml
|
|
286
|
-
on:
|
|
287
|
-
pull_request:
|
|
288
|
-
types: [opened, edited, synchronize, reopened]
|
|
289
|
-
jobs:
|
|
290
|
-
commit-identity:
|
|
291
|
-
runs-on: ubuntu-latest
|
|
292
|
-
steps:
|
|
293
|
-
- uses: actions/checkout@v5
|
|
294
|
-
with:
|
|
295
|
-
fetch-depth: 0
|
|
296
|
-
- uses: oven-sh/setup-bun@v2
|
|
297
|
-
- run: bun install --frozen-lockfile
|
|
298
|
-
- run: bunx checks-commit-identity "origin/$BASE_REF" "$HEAD_SHA"
|
|
299
|
-
env:
|
|
300
|
-
BASE_REF: ${{ github.event.pull_request.base.ref }}
|
|
301
|
-
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
The checkout fetches the full history and the range starts at the
|
|
305
|
-
fetched base branch, not the event's recorded base sha, which GitHub
|
|
306
|
-
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".
|
|
307
399
|
|
|
308
400
|
## Comment gate
|
|
309
401
|
|
|
@@ -326,27 +418,7 @@ pointer, a doc block, and a file opening with a rationale block over
|
|
|
326
418
|
three lines, licence headers excepted; `scripts/comments.ts` holds the
|
|
327
419
|
scanner the gate and the backtest share.
|
|
328
420
|
|
|
329
|
-
|
|
330
|
-
current tip to the head is visible:
|
|
331
|
-
|
|
332
|
-
```yaml
|
|
333
|
-
on:
|
|
334
|
-
pull_request:
|
|
335
|
-
types: [opened, edited, synchronize, reopened]
|
|
336
|
-
jobs:
|
|
337
|
-
comment-gate:
|
|
338
|
-
runs-on: ubuntu-latest
|
|
339
|
-
steps:
|
|
340
|
-
- uses: actions/checkout@v5
|
|
341
|
-
with:
|
|
342
|
-
fetch-depth: 0
|
|
343
|
-
- uses: oven-sh/setup-bun@v2
|
|
344
|
-
- run: bun install --frozen-lockfile
|
|
345
|
-
- run: bunx checks-comment-gate "origin/$BASE_REF" "$HEAD_SHA"
|
|
346
|
-
env:
|
|
347
|
-
BASE_REF: ${{ github.event.pull_request.base.ref }}
|
|
348
|
-
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
349
|
-
```
|
|
421
|
+
`checks-lint` runs it over each pull request's range; see "Lint entry point".
|
|
350
422
|
|
|
351
423
|
## Suppressions ratchet
|
|
352
424
|
|
|
@@ -378,27 +450,7 @@ suppressions-ratchet: 2 count(s) in oxlint-suppressions.json rose or appeared; f
|
|
|
378
450
|
src/dispatch.ts typescript/no-non-null-assertion rose from 12 to 13
|
|
379
451
|
```
|
|
380
452
|
|
|
381
|
-
|
|
382
|
-
current tip to the head is visible:
|
|
383
|
-
|
|
384
|
-
```yaml
|
|
385
|
-
on:
|
|
386
|
-
pull_request:
|
|
387
|
-
types: [opened, edited, synchronize, reopened]
|
|
388
|
-
jobs:
|
|
389
|
-
suppressions-ratchet:
|
|
390
|
-
runs-on: ubuntu-latest
|
|
391
|
-
steps:
|
|
392
|
-
- uses: actions/checkout@v5
|
|
393
|
-
with:
|
|
394
|
-
fetch-depth: 0
|
|
395
|
-
- uses: oven-sh/setup-bun@v2
|
|
396
|
-
- run: bun install --frozen-lockfile
|
|
397
|
-
- run: bunx checks-suppressions-ratchet "origin/$BASE_REF" "$HEAD_SHA"
|
|
398
|
-
env:
|
|
399
|
-
BASE_REF: ${{ github.event.pull_request.base.ref }}
|
|
400
|
-
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
401
|
-
```
|
|
453
|
+
`checks-lint` runs it over each pull request's range; see "Lint entry point".
|
|
402
454
|
|
|
403
455
|
## CI wiring
|
|
404
456
|
|
|
@@ -408,14 +460,14 @@ that: a workflow whose lint step became a no-op leaves `bun run lint`
|
|
|
408
460
|
green.
|
|
409
461
|
|
|
410
462
|
The repository declares its gates once, in `package.json`, and `lint`
|
|
411
|
-
runs the check
|
|
463
|
+
runs the check through `checks-lint`:
|
|
412
464
|
|
|
413
465
|
```json
|
|
414
466
|
"ciWiring": {
|
|
415
467
|
"gates": ["bun run lint", "bun run typecheck", "bun run test"]
|
|
416
468
|
},
|
|
417
469
|
"scripts": {
|
|
418
|
-
"lint": "oxlint --type-aware && checks-lint
|
|
470
|
+
"lint": "oxlint --type-aware && checks-lint"
|
|
419
471
|
}
|
|
420
472
|
```
|
|
421
473
|
|
|
@@ -450,8 +502,17 @@ supported way to wire a gate: it is not read, so a gate must run as a
|
|
|
450
502
|
`run:` step, such as `bunx checks-comment-gate`, in the repo's own
|
|
451
503
|
workflows.
|
|
452
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
|
+
|
|
453
513
|
The default branch is `main`; a repo with another one sets
|
|
454
|
-
`"defaultBranch"` beside `"gates"
|
|
514
|
+
`"defaultBranch"` beside `"gates"`, which `checks-lint` also reads when
|
|
515
|
+
`origin/HEAD` is not set.
|
|
455
516
|
|
|
456
517
|
It exits 1 naming each gap, with every step that runs the gate and why
|
|
457
518
|
that step does not count:
|
|
@@ -577,12 +638,23 @@ The shared Stryker preset's `json` reporter writes
|
|
|
577
638
|
not resolve `extends` in a JSON config, but a `.mjs` config that
|
|
578
639
|
spreads an imported object consumes it. Keys the consumer sets after
|
|
579
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.
|
|
580
647
|
- Each runnable script ships a `checks-` bin entry, so consumer
|
|
581
648
|
`package.json` scripts call the short name, which the package manager
|
|
582
649
|
puts on `PATH` only there; a shell runs it through `bun run`, which
|
|
583
650
|
never falls back to the registry the way `bunx` does. The `.ts` checks
|
|
584
651
|
keep a `bun` shebang, which needs no build step and no `dist/`
|
|
585
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.
|
|
586
658
|
- `checks-ci-wiring` runs inside `lint`, not in a workflow of its own:
|
|
587
659
|
deleting the step that runs a check is the violation it catches, so the
|
|
588
660
|
local `lint` is where it has to fail.
|
|
@@ -593,10 +665,11 @@ The shared Stryker preset's `json` reporter writes
|
|
|
593
665
|
`@types/bun`, which would otherwise make every runtime `bun` import look
|
|
594
666
|
like a dev-only dependency.
|
|
595
667
|
- The pull request merge commit GitHub builds is authored by `GitHub
|
|
596
|
-
<noreply@github.com>`, which commit-identity refuses as an author.
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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.
|
|
600
673
|
- `no-deep-imports` judges the import specifier, never the resolved file.
|
|
601
674
|
The base honours `exports` maps, so a subpath the map publishes resolves
|
|
602
675
|
and passes, one it omits fails to resolve and is reported, and a package
|
|
@@ -622,7 +695,7 @@ a tag off `main` and reruns the build, `dist/` check, lint, typecheck
|
|
|
622
695
|
and tests before it publishes:
|
|
623
696
|
|
|
624
697
|
```sh
|
|
625
|
-
git tag v0.
|
|
698
|
+
git tag v0.8.0 && git push origin v0.8.0
|
|
626
699
|
```
|
|
627
700
|
|
|
628
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,6 +16,10 @@
|
|
|
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",
|
|
@@ -34,6 +38,7 @@
|
|
|
34
38
|
"./commitlint.config.js": "./commitlint.config.js",
|
|
35
39
|
"./dependency-cruiser.config.js": "./dependency-cruiser.config.js",
|
|
36
40
|
"./scripts/lint-coverage.sh": "./scripts/lint-coverage.sh",
|
|
41
|
+
"./scripts/lint.ts": "./scripts/lint.ts",
|
|
37
42
|
"./scripts/test-layout.ts": "./scripts/test-layout.ts",
|
|
38
43
|
"./scripts/commit-identity.ts": "./scripts/commit-identity.ts",
|
|
39
44
|
"./scripts/mutation-compare.ts": "./scripts/mutation-compare.ts",
|
|
@@ -48,6 +53,7 @@
|
|
|
48
53
|
"./dist/index.js": "./dist/index.js"
|
|
49
54
|
},
|
|
50
55
|
"bin": {
|
|
56
|
+
"checks-lint": "scripts/lint.ts",
|
|
51
57
|
"checks-lint-coverage": "scripts/lint-coverage.sh",
|
|
52
58
|
"checks-test-layout": "scripts/test-layout.ts",
|
|
53
59
|
"checks-commit-identity": "scripts/commit-identity.ts",
|
|
@@ -59,7 +65,7 @@
|
|
|
59
65
|
},
|
|
60
66
|
"scripts": {
|
|
61
67
|
"build": "bun build effect-channel/index.ts --outdir dist --target node --format esm",
|
|
62
|
-
"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",
|
|
63
69
|
"typecheck": "tsc --noEmit && effect-tsgo diagnostics --project tsconfig.json --format text --strict",
|
|
64
70
|
"test": "bun test --randomize"
|
|
65
71
|
},
|
|
@@ -70,15 +76,14 @@
|
|
|
70
76
|
"bun run lint",
|
|
71
77
|
"bun run typecheck",
|
|
72
78
|
"bun run test",
|
|
73
|
-
"./node_modules/.bin/commitlint"
|
|
74
|
-
"bun .checks/scripts/commit-identity.ts",
|
|
75
|
-
"bun .checks/scripts/comment-gate.ts"
|
|
79
|
+
"./node_modules/.bin/commitlint"
|
|
76
80
|
]
|
|
77
81
|
},
|
|
78
82
|
"peerDependencies": {
|
|
79
83
|
"@swc/core": "1.16.2",
|
|
80
84
|
"dependency-cruiser": "18.4.0",
|
|
81
85
|
"@effect/tsgo": "0.45.0",
|
|
86
|
+
"effect": "4.0.0-rc.115",
|
|
82
87
|
"oxlint": "1.83.0",
|
|
83
88
|
"oxlint-tsgolint": "7.0.2002"
|
|
84
89
|
},
|
|
@@ -95,6 +100,8 @@
|
|
|
95
100
|
},
|
|
96
101
|
"dependencies": {
|
|
97
102
|
"@commitlint/cli": "21.2.3",
|
|
98
|
-
"@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"
|
|
99
106
|
}
|
|
100
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);
|