@avi2dg/checks 0.11.0 → 0.13.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
@@ -8,15 +8,25 @@ run that records the seeds a failing test fails with, the oxlint base
8
8
  config, the tsconfig fragment with the Effect language-service block,
9
9
  the `quality.json` schema with the generator that turns its Effect paths
10
10
  into oxlint and tsconfig fragments, the shared commitlint config, the
11
- shared dependency-cruiser base, the test-layout check with its bunfig preset, the commit-identity check, 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
12
13
  comment gate with its backtest, the oxlint suppressions ratchet, the
14
+ size budget, the feature-owner change signal and proof check, the
13
15
  Stryker mutation-testing preset with its no-regression comparator, the
14
- CI-wiring check, and the Effect error-channel plugin compiled to
15
- JavaScript.
16
+ CI-wiring check, a template for each kind of doc file with the gate that
17
+ holds each doc file to its template, and the Effect error-channel plugin
18
+ compiled to JavaScript.
16
19
 
17
20
  Published as `@avi2dg/checks` on the public npm registry.
18
21
 
19
- ## Consume it
22
+ ## Before you begin
23
+
24
+ - Bun, which runs every bin.
25
+ The kit is tested on the version its own `.bun-version` pins.
26
+ - A git repository, whose history the range gates read.
27
+ - The peer versions the install line below pins, which `peerDependencies` in `package.json` holds.
28
+
29
+ ## Install
20
30
 
21
31
  From the consuming repo:
22
32
 
@@ -48,7 +58,7 @@ node_modules/
48
58
  ```
49
59
 
50
60
  `quality.json` at the repository root declares what the repository
51
- opts into, starting with the commands its CI runs; see "Quality file"
61
+ opts into, starting with the commands its CI runs. See "Declare policy in the quality file"
52
62
  below:
53
63
 
54
64
  ```json
@@ -73,9 +83,9 @@ cp node_modules/@avi2dg/checks/bunfig.toml bunfig.toml
73
83
  ```
74
84
 
75
85
  `checks-test` runs `bun test --randomize` and fails on a skip the
76
- repository has not declared; see "Test entry point" below.
86
+ repository has not declared. See "Run the suite" below.
77
87
 
78
- `checks-lint` runs every kit gate a lint needs; see "Lint entry point"
88
+ `checks-lint` runs every kit gate a lint needs. See "Run every lint gate"
79
89
  below. Three of them:
80
90
 
81
91
  `lint-coverage.sh` fails when oxlint silently skips a tracked `.ts` or
@@ -87,7 +97,7 @@ or its config does not parse.
87
97
  `test-layout.ts` decides the test layout described below.
88
98
 
89
99
  `commit-identity.ts` refuses a commit with an author other than the
90
- repository owner; see "Commit identity" below.
100
+ repository owner. See "Check commit identities" below.
91
101
 
92
102
  A repo that runs mutation testing installs `@stryker-mutator/core` and
93
103
  `@hughescr/stryker-bun-runner`, then spreads the shipped preset in
@@ -104,7 +114,7 @@ export default {
104
114
  The registry version is pinned by the consumer's lockfile; bump
105
115
  `@avi2dg/checks` to adopt a new release.
106
116
 
107
- ## Quality file
117
+ ## Declare policy in the quality file
108
118
 
109
119
  `quality.json` at the repository root says what the repository has
110
120
  opted into. The kit's bins find it at the git root and read it there:
@@ -122,26 +132,43 @@ opted into. The kit's bins find it at the git root and read it there:
122
132
  "production": ["src/**/*.ts"],
123
133
  "effect": { "paths": ["src/**/*.ts"], "exempt": ["src/host/*.ts"] }
124
134
  },
125
- "agentRules": { "on": [], "off": [] }
135
+ "size": { "fileLines": 400, "functionLines": 100, "applies": "changed" },
136
+ "features": [
137
+ {
138
+ "name": "billing",
139
+ "root": "src/billing",
140
+ "entries": ["src/billing/index.ts"],
141
+ "allowFrom": ["src/main.ts"],
142
+ "proof": "tests/e2e/billing.test.ts"
143
+ }
144
+ ],
145
+ "changeSignal": "advisory",
146
+ "agentRules": { "on": [], "off": [] },
147
+ "docs": { "pages": { "reference": ["docs/gates/*.md"], "explanation": ["docs/design.md"] } }
126
148
  }
127
149
  ```
128
150
 
129
151
  | Key | Read by | Holds |
130
152
  | --- | --- | --- |
131
153
  | `defaultBranch` | `checks-lint`, `checks-ci-wiring` | the branch pull requests merge into, `main` when absent |
132
- | `gates.ci` | `checks-ci-wiring` | the commands CI runs on every pull request; see "CI wiring" |
154
+ | `gates.ci` | `checks-ci-wiring` | the commands CI runs on every pull request. See "Check the CI wiring" |
133
155
  | `gates.scheduled` | `checks-ci-wiring` | the commands a schedule runs |
134
- | `gates.lint` | `checks-lint`, `checks-ci-wiring` | the gates `checks-lint` runs when not all apply; see "Gate selection" |
135
- | `commitIdentity.authors` | `checks-commit-identity` | the identities allowed to author and commit; see "Commit identity" |
136
- | `sources.effect` | `checks-quality` | the paths held to the Effect rules, and the files under them that are not; see "Effect rules" |
137
- | `sources.production` | no kit gate yet | the source the repository ships |
156
+ | `gates.lint` | `checks-lint`, `checks-ci-wiring` | the gates `checks-lint` runs when not all apply. See "Gate selection" |
157
+ | `commitIdentity.authors` | `checks-commit-identity` | the identities allowed to author and commit. See "Check commit identities" |
158
+ | `sources.effect` | `checks-quality` | the paths held to the Effect rules, and the files under them that are not. See "Enforce the Effect rules" |
159
+ | `sources.production` | `checks-size-budget`, `checks-quality` | the source the repository ships. See "Hold files to a size budget" |
160
+ | `size` | `checks-size-budget` | the line budget, and which production files it holds. See "Hold files to a size budget" |
161
+ | `features` | `featureRules`, `checks-feature-owners` | each feature's root, entries, exempt importers and proof. See "Declare feature owners" |
162
+ | `changeSignal` | `checks-feature-owners` | `advisory` to list the feature owners a change touches. See "Declare feature owners" |
138
163
  | `agentRules.on`, `agentRules.off` | agent Rule selection, not the kit | catalogued Rules switched on or off for this repository |
164
+ | `docs.pages` | `checks-docs` | the Diátaxis mode of each page, by glob. See "Hold docs to their templates" |
139
165
 
140
166
  Every key is optional. The bins decode the file with one Effect
141
167
  `Schema`, and the package ships `quality.schema.json` emitted from that
142
168
  schema, so the `$schema` line gives an editor the verdict the bins
143
- reach, save one: a Rule switched both on and off, which the bins refuse
144
- and no JSON Schema can express across two lists. A key the schema does
169
+ reach, save what no JSON Schema can express across two values, which
170
+ the bins refuse: a Rule switched both on and off, a feature entry
171
+ outside its root, and two features with one name or sharing a root. A key the schema does
145
172
  not name is refused, not ignored, so a misspelt `sources` cannot switch
146
173
  the Effect rules off unnoticed. A glob in `sources`
147
174
  starts at the repository root, names a directory first, uses `*`
@@ -213,8 +240,9 @@ that changes a preset reaches the repository through its next
213
240
  `sources.effect`;
214
241
  - `.oxlintrc.json` or `tsconfig.json` does not list its fragment in
215
242
  `extends`, so the tool never reads it;
216
- - a `sources.effect.paths` glob matches no tracked or untracked file,
217
- so it holds nothing to the rules.
243
+ - a `sources.effect.paths` glob, or a `sources.production` glob while
244
+ `size` is declared, matches no tracked or untracked file, so it holds
245
+ nothing.
218
246
 
219
247
  It exits 2 when `quality.json` does not decode. `generate` writes the
220
248
  fragments, removes a left-over one, then runs the same check.
@@ -240,12 +268,12 @@ pin both:
240
268
  because one that leaves any of the kit's out turns on the category
241
269
  rules of the plugins it adds under every declared path.
242
270
 
243
- ## Lint entry point
271
+ ## Run every lint gate
244
272
 
245
273
  `checks-lint` runs each of the kit's lint gates in turn and names every
246
274
  one that fails, rather than stopping at the first. A repository whose
247
275
  tracked files give a gate nothing to check can leave it out through
248
- `gates.lint`; see "Gate selection" under "CI wiring".
276
+ `gates.lint`. See "Gate selection" under "Check the CI wiring".
249
277
 
250
278
  | Gate | Reads |
251
279
  | --- | --- |
@@ -255,7 +283,10 @@ tracked files give a gate nothing to check can leave it out through
255
283
  | `checks-comment-gate` | the range |
256
284
  | `checks-suppressions-ratchet` | the range |
257
285
  | `checks-ci-wiring` | the working tree |
286
+ | `checks-docs` | the range |
258
287
  | `checks-quality` | the working tree |
288
+ | `checks-size-budget` | the range |
289
+ | `checks-feature-owners` | the range |
259
290
 
260
291
  ```sh
261
292
  checks-lint
@@ -267,7 +298,7 @@ Locally, and on any event other than a pull request, the range ends at
267
298
  `HEAD` and starts where `HEAD` branched from the origin default branch:
268
299
  `origin/HEAD`, or when `origin/HEAD` is not set, as in an
269
300
  `actions/checkout` clone, `origin/<defaultBranch>` from the
270
- repository's `quality.json` (see "Quality file"), and `origin/main` when
301
+ repository's `quality.json`, as "Declare policy in the quality file" says, and `origin/main` when
271
302
  that is not declared. In a GitHub Actions pull request, where
272
303
  `GITHUB_EVENT_NAME` is `pull_request`, it ends
273
304
  at the event's head sha and starts where that branched from
@@ -308,13 +339,13 @@ gate's own report, then its verdict:
308
339
  ```
309
340
  checks-lint: range 2504acf098d120e73a8ece3c96f22b934f35c6a8..10ba7d8935b73ed72624120a1542e51bd21ca7c7 from HEAD against origin/main
310
341
  ...
311
- checks-lint: 3 of 7 gate(s) failed: checks-commit-identity, checks-comment-gate, checks-suppressions-ratchet
342
+ checks-lint: 3 of 9 gate(s) failed: checks-commit-identity, checks-comment-gate, checks-suppressions-ratchet
312
343
  ```
313
344
 
314
345
  It exits 1 when any gate found a violation, and 2 when the range or the
315
346
  selection does not resolve, or no failing gate could decide. ci-wiring
316
- always runs, so a repository on `checks-lint` declares `gates.ci`
317
- (see "CI wiring"), and it holds the test layout unless its selection
347
+ always runs, so a repository on `checks-lint` declares `gates.ci`,
348
+ as "Check the CI wiring" says, and it holds the test layout unless its selection
318
349
  leaves out `checks-test-layout`.
319
350
 
320
351
  CI runs it through `lint`. The checkout fetches the whole history, which
@@ -335,7 +366,7 @@ jobs:
335
366
  - run: bun run lint
336
367
  ```
337
368
 
338
- ## Effect rules
369
+ ## Enforce the Effect rules
339
370
 
340
371
  The base config loads the `effect-channel` plugin and turns on
341
372
  `effect-channel/no-error-channel-escape`, which refuses `Effect.ignore`,
@@ -356,7 +387,7 @@ through `Effect.fail`, a throwing call wrapped in `Effect.try` or
356
387
 
357
388
  A repository turns them on for the paths it writes in Effect by
358
389
  declaring those paths in `quality.json` and extending the generated
359
- fragments; see "Generated fragments" under "Quality file":
390
+ fragments. See "Generated fragments" under "Declare policy in the quality file":
360
391
 
361
392
  ```json
362
393
  "sources": {
@@ -392,7 +423,7 @@ the tsconfig fragment's override, whose severities are
392
423
  effect-tsgo keeps the severities `tsconfig.effect.json` sets when a later
393
424
  config in `extends` restates the plugin with only its overrides.
394
425
 
395
- ## Test layout
426
+ ## Lay out the tests
396
427
 
397
428
  `checks-test-layout` fails unless the repo holds this shape, and names the
398
429
  file and the path to move it to when it does not:
@@ -415,7 +446,7 @@ file and the path to move it to when it does not:
415
446
  with swc and reads import specifiers and identifier use, so a test that
416
447
  only carries `"node:child_process"` as a string is not a violation.
417
448
  - `scripts.test` is exactly `checks-test`, which runs `bun test --randomize`
418
- (see "Test entry point"), and `scripts.lint` runs this check, itself or
449
+ as "Run the suite" says, and `scripts.lint` runs this check, itself or
419
450
  through `checks-lint` called by its bare bin name.
420
451
  - `bunfig.toml` carries every `[test]` key of the shipped preset with the
421
452
  same value, and `[test].pathIgnorePatterns` is always
@@ -435,7 +466,7 @@ still run on demand:
435
466
  bun test --path-ignore-patterns='' tests/quarantine
436
467
  ```
437
468
 
438
- ## Test entry point
469
+ ## Run the suite
439
470
 
440
471
  `checks-test` runs the whole suite with `bun test --randomize`, passes
441
472
  bun's output through, and then reads bun's JUnit report of the same run.
@@ -478,10 +509,10 @@ passed without writing its report. It takes no arguments: a `-t`
478
509
  filter reports every test it leaves out as skipped and a path filter
479
510
  drops files a declaration names, so a narrowed run is plain
480
511
  `bun test --randomize` with the arguments. Files under
481
- `tests/quarantine/` are never run and so never reported; see "Test
512
+ `tests/quarantine/` are never run and so never reported. See "Test
482
513
  layout".
483
514
 
484
- ## Flake run
515
+ ## Find flaky tests
485
516
 
486
517
  A green run proves nothing failed in that run, not that no test is
487
518
  flaky. `checks-flake` runs the whole suite several times, each with its
@@ -536,9 +567,9 @@ jobs:
536
567
  ```
537
568
 
538
569
  and declares the step in `gates.scheduled`, so `checks-ci-wiring`
539
- fails once the schedule stops running it; see "CI wiring".
570
+ fails once the schedule stops running it. See "Check the CI wiring".
540
571
 
541
- ## Dependency rules
572
+ ## Enforce dependency rules
542
573
 
543
574
  `.dependency-cruiser.cjs` extends the shared base, which carries
544
575
  `no-circular`, `no-orphans`, `not-to-dev-dep` (shipped source importing
@@ -566,7 +597,9 @@ boundary you own. A rule that restates a base name overrides it field
566
597
  by field, which is how an entry point stops being an orphan:
567
598
  redeclare `no-orphans` with your entry added to its `pathNot`.
568
599
  This repo's own `.dependency-cruiser.cjs` does that for the plugin
569
- entry.
600
+ entry. A repository that declares feature owners spreads the rules
601
+ `quality.json` compiles to into the same `forbidden`. See "Feature
602
+ owners".
570
603
 
571
604
  `package.json` gains the script:
572
605
 
@@ -586,7 +619,7 @@ jobs:
586
619
  - run: bun run lint:deps
587
620
  ```
588
621
 
589
- ## Commit lint
622
+ ## Lint commit messages
590
623
 
591
624
  Commits follow `@commitlint/config-conventional` plus the house
592
625
  prefixes listed in `commitlint.config.js`, shared from
@@ -625,7 +658,7 @@ It never sees a commit's author or committer fields, nor the
625
658
  squashes, so it cannot enforce who a commit belongs to. The
626
659
  commit-identity check below is the enforcement.
627
660
 
628
- ## Commit identity
661
+ ## Check commit identities
629
662
 
630
663
  `scripts/commit-identity.ts` walks every commit in a range and fails when
631
664
  one carries an identity other than the repository owner's:
@@ -651,9 +684,10 @@ owners restates it in `quality.json`:
651
684
  }
652
685
  ```
653
686
 
654
- `checks-lint` runs it over each pull request's range; see "Lint entry point".
687
+ `checks-lint` runs it over each pull request's range.
688
+ See "Run every lint gate".
655
689
 
656
- ## Comment gate
690
+ ## Refuse banned comments
657
691
 
658
692
  `scripts/comment-gate.ts` runs the comment check over a diff and fails
659
693
  when an added line carries a banned comment:
@@ -682,9 +716,10 @@ and import it as `@avi2dg/checks/scripts/comment-matchers.ts`. The repo's
682
716
  cruise fails when it gains an import. `scripts/comments.ts` wraps the
683
717
  same matchers in Effect for the gate and the backtest.
684
718
 
685
- `checks-lint` runs it over each pull request's range; see "Lint entry point".
719
+ `checks-lint` runs it over each pull request's range.
720
+ See "Run every lint gate".
686
721
 
687
- ## Suppressions ratchet
722
+ ## Ratchet the suppressions
688
723
 
689
724
  `scripts/suppressions-ratchet.ts` holds oxlint's bulk-suppression
690
725
  baseline, `oxlint-suppressions.json`, to counts that only fall. oxlint
@@ -715,9 +750,234 @@ suppressions-ratchet: 2 count(s) in oxlint-suppressions.json rose or appeared; f
715
750
  src/dispatch.ts typescript/no-non-null-assertion rose from 12 to 13
716
751
  ```
717
752
 
718
- `checks-lint` runs it over each pull request's range; see "Lint entry point".
753
+ `checks-lint` runs it over each pull request's range.
754
+ See "Run every lint gate".
755
+
756
+ ## Hold files to a size budget
757
+
758
+ `checks-size-budget` holds production files to the line budget
759
+ `quality.json` declares, and lists every other file over it without
760
+ failing:
761
+
762
+ ```json
763
+ "sources": { "production": ["src/**/*.ts"] },
764
+ "size": { "fileLines": 400, "functionLines": 100, "applies": "changed" }
765
+ ```
766
+
767
+ ```sh
768
+ checks-size-budget <base-ref> <head-ref>
769
+ checks-size-budget <ref>
770
+ ```
771
+
772
+ It runs oxlint with a configuration of two rules and nothing else:
773
+ `max-lines` at `fileLines` and `max-lines-per-function` at
774
+ `functionLines`, both counting blank and comment lines. With `applies`
775
+ set to `changed` it holds the files under `sources.production` that the
776
+ range adds or changes, a rename that edits the file included. With
777
+ `all` it holds every file under `sources.production`. A file the range
778
+ deletes or only renames is not held. Every other tracked `.ts` or
779
+ `.tsx` file over the budget, tests and unchanged production files alike,
780
+ is listed as advisory and never fails the gate; `.d.ts` files are not
781
+ measured.
782
+
783
+ It reads each file from the head commit rather than the working tree,
784
+ so an uncommitted edit neither fails nor passes a range, and a pull
785
+ request's merge checkout measures what the pull request holds. With two
786
+ arguments the range starts where the head branched from the base, at
787
+ their merge-base. With one it is that commit against its parent, or
788
+ against the empty tree for a repository's first commit. oxlint must be
789
+ on `PATH`, as it is under a package script.
790
+
791
+ ```
792
+ size-budget: 1 overrun(s) of 400 lines per file and 100 per function in the production files the range adds or changes:
793
+ src/billing/ledger.ts:12: The function `settle` has too many lines (131). Maximum allowed is 100.
794
+ size-budget: advisory, 1 overrun(s) where the budget does not hold yet:
795
+ tests/e2e/billing.test.ts: File has too many lines (512).
796
+ ```
797
+
798
+ It exits 1 on an overrun in a file it holds, and 2 when `quality.json`
799
+ does not decode, a ref does not resolve or oxlint cannot run. A
800
+ repository that declares no `size` passes. `quality.json` refuses a
801
+ `size` without `sources.production`, which would hold nothing, and
802
+ with `size` declared `checks-quality` refuses a `sources.production`
803
+ glob that matches no file. Moving `applies` from `changed` to `all`
804
+ tightens the budget to every production file, once the advisory list
805
+ names none.
806
+
807
+ `checks-lint` runs it over each pull request's range.
808
+ See "Run every lint gate".
809
+
810
+ ## Declare feature owners
811
+
812
+ A repository opts a feature in by declaring, in `quality.json`, the
813
+ directory it owns, the files code outside it imports it through, the
814
+ files that may reach past those, and the end-to-end test that proves it
815
+ runs:
816
+
817
+ ```json
818
+ "features": [
819
+ {
820
+ "name": "billing",
821
+ "root": "src/billing",
822
+ "entries": ["src/billing/index.ts"],
823
+ "allowFrom": ["src/main.ts", "src/cli/*.ts"],
824
+ "proof": "tests/e2e/billing.test.ts"
825
+ }
826
+ ],
827
+ "changeSignal": "advisory"
828
+ ```
829
+
830
+ `root` is a directory and `entries` are files under it, both without
831
+ globs. `allowFrom` holds globs of the same shape as `sources`. `proof`
832
+ is a `.test.ts` or `.test.tsx` file under `tests/e2e/`. Nothing moves:
833
+ a root is wherever the feature already lives. `quality.json` refuses an
834
+ entry outside its root, a name used twice, and two features sharing a
835
+ root or one root inside another, so a file has at most one owner.
836
+
837
+ ### Import boundary
719
838
 
720
- ## CI wiring
839
+ `dist/feature-rules.js` compiles `features` into one dependency-cruiser
840
+ rule per feature, which `.dependency-cruiser.cjs` spreads beside its
841
+ own:
842
+
843
+ ```js
844
+ const { featureRules } = require("@avi2dg/checks/dist/feature-rules.js");
845
+
846
+ module.exports = {
847
+ extends: "./node_modules/@avi2dg/checks/dependency-cruiser.config.js",
848
+ forbidden: [...featureRules(require("./quality.json"))],
849
+ };
850
+ ```
851
+
852
+ A module outside a feature's root that imports a file inside it must
853
+ import one of the feature's `entries`. Modules under `tests/` and the
854
+ files `allowFrom` matches, such as a CLI or a harness, may import any
855
+ file in it:
856
+
857
+ ```
858
+ error feature-billing-entries: src/report.ts → src/billing/charge.ts
859
+ ```
860
+
861
+ `featureRules` decodes its argument with the schema the bins use and
862
+ throws the schema's refusal when it does not decode, which stops the
863
+ cruise. It is an ES module, as `effect` is, so a `.cjs` config loads it
864
+ through `require`, which needs node 20.19, 22.12 or later.
865
+
866
+ ### Change signal and proof
867
+
868
+ `checks-feature-owners` reads the same declaration over a range:
869
+
870
+ ```sh
871
+ checks-feature-owners <base-ref> <head-ref>
872
+ checks-feature-owners <ref>
873
+ ```
874
+
875
+ It exits 1 when a feature's proof cannot prove it: the proof or an
876
+ entry is not in the head commit, the proof does not parse, or it
877
+ imports none of the feature's entries. An import counts when it is a
878
+ runtime `import`, `export ... from` or `export * from` of a relative
879
+ path that names an entry: by its own name, by the `.js`, `.jsx`, `.mjs`
880
+ or `.cjs` spelling of it, `.js` naming a `.tsx` entry as well as a
881
+ `.ts` one, or without an extension, the way a directory `index` is
882
+ imported. `import type` does not count, and neither does a
883
+ path alias. The proof runs in `bun run test` like any end-to-end test,
884
+ which is what shows it passes.
885
+
886
+ ```
887
+ feature-owners: 1 problem(s) with the features' runnable proofs:
888
+ billing: proof tests/e2e/billing.test.ts imports none of its entries, src/billing/index.ts
889
+ ```
890
+
891
+ With `changeSignal` set to `advisory` it also lists each owner the
892
+ range touches, with the paths it touched under the owner's root or at
893
+ its proof, and still exits 0. Whether a change that spans owners is one
894
+ coherent slice is for a reviewer to judge. A rename counts at both of
895
+ its paths:
896
+
897
+ ```
898
+ feature-owners: advisory, the range touches 2 feature owner(s); a reviewer judges whether they make one slice:
899
+ billing: src/billing/charge.ts, src/billing/tax.ts
900
+ invoices: src/invoices/tax.ts, tests/e2e/invoices.test.ts
901
+ ```
902
+
903
+ It exits 2 when `quality.json` does not decode, which is where a proof
904
+ outside `tests/e2e/` is refused, or a ref does not resolve. A
905
+ repository that declares no feature passes, and `quality.json` refuses
906
+ a `changeSignal` without features, which would map a change to no
907
+ owner.
908
+
909
+ `checks-lint` runs it over each pull request's range.
910
+ See "Run every lint gate".
911
+
912
+ ## Hold docs to their templates
913
+
914
+ `checks-docs` holds each doc file a change touches to the template for its kind, and lists every other doc file that does not conform yet without failing.
915
+ The package ships one template per kind under `templates/`, and a repository starts a new doc file by copying one:
916
+
917
+ ```sh
918
+ cp node_modules/@avi2dg/checks/templates/how-to.md docs/add-a-supplier.md
919
+ ```
920
+
921
+ | File | Kind | Template |
922
+ | --- | --- | --- |
923
+ | `README.md` | readme | `templates/readme.md` |
924
+ | `CHANGELOG.md` | changelog | `templates/changelog.md` |
925
+ | `AGENTS.md` | agents | `templates/agents.md` |
926
+ | `CLAUDE.md` | claude | `templates/claude.md` |
927
+ | each file in `docs/adr/` but its generated index, `README.md` | adr | `templates/adr.md` |
928
+ | a page `docs.pages` declares | tutorial, how-to, reference or explanation | `templates/<mode>.md` |
929
+
930
+ The first four are the files at the repository root.
931
+ No other Markdown file is judged, save a page under `docs/`, which needs a mode.
932
+ Which Diátaxis mode a page is written in is a judgment, so `quality.json` declares it:
933
+
934
+ ```json
935
+ "docs": {
936
+ "pages": {
937
+ "reference": ["docs/gates/*.md"],
938
+ "explanation": ["docs/design.md"]
939
+ }
940
+ }
941
+ ```
942
+
943
+ A template decides a file's structure, and the template file itself is the reference for each kind:
944
+
945
+ - A file opens with one `# ` title on its first line and has text before its first section.
946
+ It skips no heading level, and no heading is Overview, Introduction or How it works.
947
+ - Its sections are the template's headings in the template's order.
948
+ A heading in angle brackets is one the writer names.
949
+ One marked verb first is left to review, since no program tells a verb from a noun there.
950
+ A heading the template does not have, in that place, is refused.
951
+ - A record in `docs/adr/` is named for its four-digit number, and its title opens with the same number.
952
+ A `Date: YYYY-MM-DD` line follows the title, the first word under Status is Proposed, Accepted, Rejected, Deprecated, Superseded or Retired, and no other record holds its number.
953
+ - A changelog lists its releases newest first, each opening with a `Released YYYY-MM-DD.` line.
954
+ - A how-to or tutorial page numbers its steps.
955
+ - `CLAUDE.md` is its template word for word.
956
+
957
+ ```sh
958
+ checks-docs <base-ref> <head-ref>
959
+ checks-docs <ref>
960
+ ```
961
+
962
+ It reads each file from the head commit.
963
+ With two arguments the range starts where the head branched from the base, at their merge-base.
964
+ With one it is that commit against its parent, or against the empty tree for a repository's first commit.
965
+ A file the range adds, changes or renames is held to its template, and a file it deletes is not.
966
+
967
+ ```
968
+ docs: 2 violation(s) in the doc files the range touches:
969
+ README.md:1: lacks `## Where things are`
970
+ docs/parts.md: is a page under docs/ with no mode; declare it under docs.pages in quality.json as tutorial, how-to, reference, explanation
971
+ docs: advisory, 1 doc file(s) the range leaves alone do not hold to their templates yet:
972
+ docs/adr/0001-quality-gates.md: 5 violation(s)
973
+ ```
974
+
975
+ It exits 1 on a violation in a file the range touches, and 2 when `quality.json` does not decode or a ref does not resolve.
976
+
977
+ `checks-lint` runs it over each pull request's range.
978
+ See "Run every lint gate".
979
+
980
+ ## Check the CI wiring
721
981
 
722
982
  `checks-ci-wiring` fails when a command the repository's CI must run no
723
983
  longer runs on pull requests to the default branch. No local check sees
@@ -820,8 +1080,9 @@ ci-wiring: 1 of 1 scheduled command(s) do not run on a schedule:
820
1080
 
821
1081
  ### Gate selection
822
1082
 
823
- A repository with no TypeScript source gives `checks-lint-coverage` and
824
- `checks-test-layout` nothing to check, and test-layout still refuses its
1083
+ A repository with no TypeScript source gives `checks-lint-coverage`,
1084
+ `checks-test-layout`, `checks-size-budget` and `checks-feature-owners`
1085
+ nothing to check, and test-layout still refuses its
825
1086
  missing `bun test` script and `bunfig.toml`. It declares the gates
826
1087
  `checks-lint` runs as `gates.lint`:
827
1088
 
@@ -833,13 +1094,14 @@ missing `bun test` script and `bunfig.toml`. It declares the gates
833
1094
  "checks-comment-gate",
834
1095
  "checks-suppressions-ratchet",
835
1096
  "checks-ci-wiring",
1097
+ "checks-docs",
836
1098
  "checks-quality"
837
1099
  ]
838
1100
  }
839
1101
  ```
840
1102
 
841
- `checks-lint` runs exactly those, in the "Lint entry point" table's
842
- order, and all seven when `gates.lint` is absent. A selection in
1103
+ `checks-lint` runs exactly those, in the "Run every lint gate" table's
1104
+ order, and all ten when `gates.lint` is absent. A selection in
843
1105
  `quality.json` always keeps `checks-quality`, since the file it sits in
844
1106
  is what makes that gate apply. A step running
845
1107
  `checks-lint` then counts only for a declared gate that `gates.lint`
@@ -855,7 +1117,10 @@ A selection may leave out only a gate that does not apply:
855
1117
  | `checks-comment-gate` | always |
856
1118
  | `checks-suppressions-ratchet` | always |
857
1119
  | `checks-ci-wiring` | always |
1120
+ | `checks-docs` | always |
858
1121
  | `checks-quality` | tracks a `quality.json` |
1122
+ | `checks-size-budget` | tracks a `.ts` or `.tsx` file |
1123
+ | `checks-feature-owners` | tracks a `.ts` or `.tsx` file |
859
1124
 
860
1125
  Both bins exit 2 on a `gates.lint` that names an unknown gate or leaves
861
1126
  out one that always applies. ci-wiring exits 1 when the
@@ -863,9 +1128,11 @@ selection leaves out a gate the repository's tracked files make
863
1128
  applicable, and names the gate and the files:
864
1129
 
865
1130
  ```
866
- ci-wiring: quality.json gates.lint leaves out 2 gate(s) this repository's contents make applicable:
1131
+ ci-wiring: quality.json gates.lint leaves out 4 gate(s) this repository's contents make applicable:
867
1132
  checks-lint-coverage: the repository tracks TypeScript source (src/widget.ts)
868
1133
  checks-test-layout: the repository tracks TypeScript source (src/widget.ts)
1134
+ checks-size-budget: the repository tracks TypeScript source (src/widget.ts)
1135
+ checks-feature-owners: the repository tracks TypeScript source (src/widget.ts)
869
1136
  ```
870
1137
 
871
1138
  It reads the files tracked at the checkout, so the pull request that
@@ -886,7 +1153,7 @@ does not evaluate:
886
1153
  command itself does, the shell's options, and a step or job that
887
1154
  fails or times out before the gate step.
888
1155
 
889
- ## Backtest
1156
+ ## Backtest the comment check
890
1157
 
891
1158
  `scripts/backtest.ts` reports what the comment check would have refused
892
1159
  at each recent commit, so a repository can measure its own history:
@@ -902,7 +1169,7 @@ comment text as a share of added lines.
902
1169
  `generated/`, `vendor/`, `repos/`, `node_modules/` and `dist/` are out
903
1170
  of reach, so the figures are authored code.
904
1171
 
905
- ## Mutation compare
1172
+ ## Compare mutation scores
906
1173
 
907
1174
  `checks-mutation-compare` gates a pull request on no-regression rather
908
1175
  than an absolute threshold: the head mutation score may not fall below
@@ -948,107 +1215,6 @@ jobs:
948
1215
  The shared Stryker preset's `json` reporter writes
949
1216
  `reports/mutation/mutation.json` in each worktree.
950
1217
 
951
- ## Why it is shaped this way
952
-
953
- - Every config in an oxlint `extends` chain brings its own `plugins`,
954
- and one that sets none brings oxlint's default plugins, whose
955
- category rules the base's `categories` then turn on across the tree.
956
- `rules`, `categories` and `jsPlugins` inherit as expected. That is why
957
- the consumer snippet restates `plugins` and nothing else, and why the
958
- generated fragment always sets them.
959
- - `node_modules/` is excluded through the consumer's `.gitignore`, not
960
- `ignorePatterns`: oxlint still walks the installed package when only
961
- `ignorePatterns` names it.
962
- - `files` in package.json is the published surface: `tests/`, `AGENTS.md`
963
- and the `.ts` plugin source never reach an install. npm adds
964
- `package.json`, `README` and `LICENSE` to the tarball whatever `files`
965
- says. `bun pm pack` builds the same tarball the registry serves, which
966
- is what the packed-tarball consumer e2e test installs.
967
- - The plugin ships compiled as `dist/index.js`, built with
968
- `bun build effect-channel/index.ts --outdir dist --target node --format esm`.
969
- Node refuses to type-strip a `.ts` plugin under `node_modules`, so the
970
- `.ts` source would fail to load from an installed package.
971
- - `dist/` is committed. No `prepack` or `prepublishOnly` builds it, so a
972
- publish ships whatever bundle the publishing worktree holds. Rebuild it
973
- after pulling with `bun run build`; CI fails when the committed bundle
974
- is stale. `bun run build` also emits `quality.schema.json`, which is
975
- committed the same way, and a test fails when it differs from what
976
- the schema emits.
977
- - `quality.json` is JSON, not TOML or a TypeScript module: a bun bin, a
978
- hook running without `node_modules`, a `.cjs` or `.mjs` config and
979
- `jq` all parse it with nothing installed, and nobody runs a
980
- repository's own code to learn its policy. It holds declarations
981
- only. The kit's bins read it directly; oxlint and tsc read nothing but
982
- their own JSON, so they extend generated fragments, which
983
- `checks-quality --check` holds to the declarations.
984
- - `quality.json` refuses a key its schema does not name, so a kit that
985
- cannot enforce a newer key refuses it rather than let the repository
986
- believe it enforced.
987
- - The base parses with swc because typescript 7 (tsgo) has no compiler
988
- API for dependency-cruiser to use. Without `@swc/core` installed the
989
- cruise silently skips every `.ts` file, so this repo's test asserts its
990
- own TypeScript is cruised.
991
- - `bunfig.toml` has no `extends` and no include: bun ignores an unknown
992
- top-level key in silence, so a preset cannot be inherited and the
993
- consumer's copy is compared key by key against the installed one
994
- instead. `[test] pathIgnorePatterns` is a real bunfig key, and an empty
995
- `--path-ignore-patterns` flag overrides the file's own list.
996
- - The Stryker preset is a JavaScript module, not JSON: Stryker 10 does
997
- not resolve `extends` in a JSON config, but a `.mjs` config that
998
- spreads an imported object consumes it. Keys the consumer sets after
999
- the spread win.
1000
- - The `.ts` bins are written in Effect, so `effect` is a peer dependency
1001
- and `@effect/platform-bun`, which only the bins use, is a dependency.
1002
- `@effect/platform-node-shared` is a direct dependency at the same exact
1003
- version only to pin it: `@effect/platform-bun` asks for it with a `^`
1004
- range, and a newer rc peers on a newer `effect` than consumers install,
1005
- so all three move together.
1006
- - Each runnable script ships a `checks-` bin entry, so consumer
1007
- `package.json` scripts call the short name, which the package manager
1008
- puts on `PATH` only there; a shell runs it through `bun run`, which
1009
- never falls back to the registry the way `bunx` does. The `.ts` checks
1010
- keep a `bun` shebang, which needs no build step and no `dist/`
1011
- entry, unlike the oxlint plugin that node loads.
1012
- - `checks-lint` runs each gate as its own bin in a child process rather
1013
- than importing it, so a gate behaves the same called alone or through
1014
- the entry point, and `lint-coverage.sh` stays a shell script. The
1015
- gates run one at a time with their output passed straight through, so
1016
- each report reads whole and in the table's order.
1017
- - A gate selection is checked against the repository's contents rather
1018
- than trusted, so it cannot skip a gate that applies. ci-wiring does
1019
- that check, which is why a selection without it, or without another
1020
- gate that applies everywhere, is refused as `checks-lint` reads it:
1021
- nothing would check the selection otherwise.
1022
- - `checks-test` runs bun itself rather than reading a report some other
1023
- run left: a skip taken only on CI is visible only in CI's own run, and
1024
- an earlier run's report may be stale or narrowed. It reads the JUnit
1025
- report bun writes to a temporary directory, since bun has no other
1026
- per-test output meant for a program.
1027
- - `checks-ci-wiring` runs inside `lint`, not in a workflow of its own:
1028
- deleting the step that runs a check is the violation it catches, so the
1029
- local `lint` is where it has to fail.
1030
- - Workflows are parsed with `Bun.YAML`, which the `bun` shebang already
1031
- provides, so the check adds no dependency. It reads `on` as a string
1032
- key, not as the YAML 1.1 boolean.
1033
- - `bun` counts as a built-in module. Nothing installed resolves it except
1034
- `@types/bun`, which would otherwise make every runtime `bun` import look
1035
- like a dev-only dependency.
1036
- - The pull request merge commit GitHub builds is authored by `GitHub
1037
- <noreply@github.com>`, which commit-identity refuses as an author.
1038
- `checks-lint` ends a pull request's range at the event's head sha, so
1039
- the merge commit is never in it. A `lint` that calls
1040
- `checks-commit-identity HEAD` itself checks out
1041
- `github.event.pull_request.head.sha` instead of the default merge ref.
1042
- - `no-deep-imports` judges the import specifier, never the resolved file.
1043
- The base honours `exports` maps, so a subpath the map publishes resolves
1044
- and passes, one it omits fails to resolve and is reported, and a package
1045
- without an `exports` map publishes every file. A bare import always
1046
- passes whatever file its entry lives in. Setting your own
1047
- `options.enhancedResolveOptions` replaces the base's, so restate
1048
- `exportsFields` and `conditionNames` if you do.
1049
- - dependency-cruiser `extends` merges same-name `forbidden` rules with the
1050
- child's fields winning. That is the entry-point and layer recipe above.
1051
-
1052
1218
  ## Develop
1053
1219
 
1054
1220
  ```sh
@@ -1082,3 +1248,20 @@ every later tag publishes through the workflow.
1082
1248
 
1083
1249
  `publishConfig.access` in package.json is what makes the scoped package
1084
1250
  public.
1251
+
1252
+ ## Where things are
1253
+
1254
+ | Path | What it holds |
1255
+ | --- | --- |
1256
+ | `scripts/` | every bin, and the modules they share |
1257
+ | `effect-channel/` | the Effect error-channel oxlint plugin |
1258
+ | `dist/` | the committed bundles of the plugin and of `featureRules` |
1259
+ | `presets/` | the Effect presets `checks-quality` builds its fragments from |
1260
+ | `templates/` | one template per kind of doc file, which `bun run build` renders |
1261
+ | `tests/` | the suite, with the tests that spawn a process under `tests/e2e/` |
1262
+ | `docs/` | pages for whoever develops the kit |
1263
+ | the root configs | `oxlintrc.json`, `tsconfig.effect.json`, `bunfig.toml`, `commitlint.config.js`, `dependency-cruiser.config.js`, `stryker.preset.js` and `quality.schema.json`, which a consuming repository extends or copies |
1264
+
1265
+ ## Related topics
1266
+
1267
+ - [Why it is shaped this way](docs/design.md)