@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 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 oxlint base config, the tsconfig fragment with the
5
- Effect language-service block, the shared commitlint config, the shared
6
- dependency-cruiser base, the test-layout check with its bunfig preset,
7
- the commit-identity check with its workflow, the comment gate with its
8
- workflow and backtest, the Stryker mutation-testing preset with its
9
- no-regression comparator, the CI-wiring check, and the Effect
10
- error-channel plugin compiled to 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.
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-coverage && checks-test-layout && checks-commit-identity HEAD",
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, which is the form the
265
- `lint` script above runs on `HEAD`. It refuses a commit whose author or
266
- committer is outside the allowlist, and one whose trailer block carries
267
- a `Co-authored-by:` trailer as git parses it, and it names the offending
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
- Enforcement runs on pull requests, where the range from the base branch's
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
- Enforcement runs on pull requests, where the range from the base branch's
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
- ```yaml
332
- on:
333
- pull_request:
334
- types: [opened, edited, synchronize, reopened]
335
- jobs:
336
- comment-gate:
337
- runs-on: ubuntu-latest
338
- steps:
339
- - uses: actions/checkout@v5
340
- with:
341
- fetch-depth: 0
342
- - uses: oven-sh/setup-bun@v2
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-coverage && checks-test-layout && checks-ci-wiring"
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. A
544
- consumer that runs the check inside `lint` checks out
545
- `github.event.pull_request.head.sha` instead of the default merge ref,
546
- as this repo's `ci.yml` does.
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.5.0 && git push origin v0.5.0
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.5.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 && ./scripts/lint-coverage.sh && bun scripts/test-layout.ts && bun scripts/commit-identity.ts HEAD && bun scripts/comment-gate.ts HEAD && bun scripts/ci-wiring.ts && depcruise --config .dependency-cruiser.cjs effect-channel scripts tests commitlint.config.js dependency-cruiser.config.js stryker.preset.js .dependency-cruiser.cjs",
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
  }
@@ -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
- async function at(sha: string, path: string): Promise<string | undefined> {
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 async function backtest(count: string): Promise<string> {
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 = await at(sha, path);
75
- if (after === undefined) continue;
76
- const before = (await at(parent, path)) ?? "";
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
- if (import.meta.main) {
100
+ const main = Effect.gen(function* () {
114
101
  const [count = "60", ...extra] = process.argv.slice(2);
115
- if (extra.length > 0) die(USAGE);
116
- console.log(await backtest(count));
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);