@avi2dg/checks 0.17.0 → 0.19.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +3 -1
  3. package/bunfig.toml +1 -1
  4. package/dependency-cruiser.config.js +2 -0
  5. package/dist/feature-rules.js +22 -5
  6. package/docs/configs/commit-messages.md +2 -18
  7. package/docs/configs/quality-file.md +3 -2
  8. package/docs/gates/checks-lint.md +2 -1
  9. package/docs/gates/checks-mutation-compare.md +20 -11
  10. package/docs/gates/checks-quality.md +23 -8
  11. package/docs/gates/checks-quarantine-clock.md +54 -0
  12. package/docs/gates/checks-test-layout.md +9 -3
  13. package/docs/gates/checks-vendor.md +97 -0
  14. package/package.json +11 -2
  15. package/quality.schema.json +56 -4
  16. package/scripts/backtest.ts +57 -41
  17. package/scripts/ci-wiring.ts +2 -63
  18. package/scripts/comment-gate.ts +3 -4
  19. package/scripts/comment-matchers.ts +54 -57
  20. package/scripts/commit-identity.ts +3 -4
  21. package/scripts/doc-outline.ts +34 -16
  22. package/scripts/doc-rules.ts +34 -21
  23. package/scripts/docs.ts +3 -4
  24. package/scripts/feature-owners.ts +5 -8
  25. package/scripts/gates.ts +1 -0
  26. package/scripts/git.ts +41 -26
  27. package/scripts/lint.ts +75 -35
  28. package/scripts/mutation-compare.ts +179 -70
  29. package/scripts/prose-matchers.ts +93 -83
  30. package/scripts/quality-file.ts +30 -3
  31. package/scripts/quality.ts +157 -10
  32. package/scripts/quarantine-clock.ts +153 -0
  33. package/scripts/range-gate.ts +9 -0
  34. package/scripts/repetition.ts +40 -19
  35. package/scripts/shell-command.ts +74 -0
  36. package/scripts/size-budget.ts +51 -28
  37. package/scripts/suppressions-ratchet.ts +22 -28
  38. package/scripts/test-layout.ts +65 -52
  39. package/scripts/test-report.ts +18 -13
  40. package/scripts/vendor.ts +337 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  Every release of `@avi2dg/checks`, newest first, written by the release from its conventional commits.
4
4
 
5
+ ## 0.19.0
6
+
7
+ Released 2026-09-26.
8
+
9
+ ### Features
10
+
11
+ - **scripts:** judge checks-mutation-compare mutant by mutant instead of by score (#58)
12
+ - **scripts:** fail a test left in tests/quarantine past 30 days (#59)
13
+ - **scripts:** pin shared read-only library clones with checks-vendor (#57)
14
+
15
+ ## 0.18.0
16
+
17
+ Released 2026-09-26.
18
+
19
+ ### Features
20
+
21
+ - **scripts:** generate commitlint and CI workflows with checks-quality (#54)
22
+
23
+ ### Fixes
24
+
25
+ - **scripts:** make checks-quality refuse a config that drops the kit's extends (#55)
26
+
5
27
  ## 0.17.0
6
28
 
7
29
  Released 2026-09-25.
package/README.md CHANGED
@@ -123,6 +123,7 @@ A repository leaves out a gate that does not apply to it through `gates.lint`, a
123
123
  | [`checks-size-budget`](docs/gates/checks-size-budget.md) | the range | a repository tracking `*.ts` or `*.tsx` |
124
124
  | [`checks-repetition`](docs/gates/checks-repetition.md) | the range | a repository tracking `*.ts` or `*.tsx` |
125
125
  | [`checks-feature-owners`](docs/gates/checks-feature-owners.md) | the range | a repository tracking `*.ts` or `*.tsx` |
126
+ | [`checks-quarantine-clock`](docs/gates/checks-quarantine-clock.md) | the range | every repository |
126
127
 
127
128
  <!-- end generated gates -->
128
129
 
@@ -130,8 +131,9 @@ These bins run on their own:
130
131
 
131
132
  - [`checks-test`](docs/gates/checks-test.md) runs the suite as `scripts.test` and refuses a skip the repository has not declared.
132
133
  - [`checks-flake`](docs/gates/checks-flake.md) runs the suite on a schedule and records the seeds a flaky test fails with.
133
- - [`checks-mutation-compare`](docs/gates/checks-mutation-compare.md) holds a pull request's mutation score to no regression.
134
+ - [`checks-mutation-compare`](docs/gates/checks-mutation-compare.md) holds every mutant in a pull request to no regression.
134
135
  - [`checks-backtest`](docs/gates/checks-backtest.md) reports what the comment check would have refused in recent history.
136
+ - [`checks-vendor`](docs/gates/checks-vendor.md) pins each library `quality.json` declares to a shared read-only clone and links it under `repos/`.
135
137
 
136
138
  `checks-lint` has [its own page](docs/gates/checks-lint.md), which says which range it resolves.
137
139
  The oxlint base, the dependency-cruiser base and the commitlint config run through their own tools, as the pages under Related topics say.
package/bunfig.toml CHANGED
@@ -1,2 +1,2 @@
1
1
  [test]
2
- pathIgnorePatterns = ["**/tests/quarantine/**"]
2
+ pathIgnorePatterns = ["**/tests/quarantine/**", "repos/**"]
@@ -72,6 +72,8 @@ export default {
72
72
  parser: "swc",
73
73
  builtInModules: { add: ["bun"] },
74
74
  doNotFollow: { path: ["node_modules"] },
75
+ // The cruise follows the repos/ links without this.
76
+ exclude: { path: "^repos/" },
75
77
  enhancedResolveOptions: {
76
78
  exportsFields: ["exports"],
77
79
  conditionNames: ["types", "import", "require", "node", "default"],
@@ -21,7 +21,8 @@ var KIT_GATES = [
21
21
  { bin: "checks-quality", script: "quality.ts", reads: "tree", args: ["--check"], appliesTo: QUALITY_DECLARATION },
22
22
  { bin: "checks-size-budget", script: "size-budget.ts", reads: "range", appliesTo: TYPESCRIPT_SOURCE },
23
23
  { bin: "checks-repetition", script: "repetition.ts", reads: "range", appliesTo: TYPESCRIPT_SOURCE },
24
- { bin: "checks-feature-owners", script: "feature-owners.ts", reads: "range", appliesTo: TYPESCRIPT_SOURCE }
24
+ { bin: "checks-feature-owners", script: "feature-owners.ts", reads: "range", appliesTo: TYPESCRIPT_SOURCE },
25
+ { bin: "checks-quarantine-clock", script: "quarantine-clock.ts", reads: "range", appliesTo: EVERY_REPOSITORY }
25
26
  ];
26
27
  var UNCONDITIONAL = KIT_GATES.filter((gate) => gate.appliesTo === EVERY_REPOSITORY).map((gate) => gate.bin);
27
28
  var LintGates = Schema.Array(Schema.Literals(KIT_GATES.map((gate) => gate.bin))).check(Schema.makeFilter((selected) => {
@@ -158,8 +159,21 @@ var EffectSources = Schema3.Struct({
158
159
  }),
159
160
  exempt: Schema3.optionalKey(Schema3.Array(PathGlob).annotate({ description: "Files under paths the Effect rules pass over" }))
160
161
  });
162
+ var Library = Schema3.Struct({
163
+ name: Schema3.String.check(Schema3.isPattern(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, { expected: "a library name in kebab case" })).annotate({ description: "The link checks-vendor manages under repos/" }),
164
+ package: Schema3.NonEmptyString.annotate({ description: "The npm package whose installed version picks the tag" }),
165
+ repository: Schema3.NonEmptyString.annotate({ description: "The git remote checks-vendor clones the tag from" }),
166
+ tag: Schema3.String.check(Schema3.isPattern(/\{version\}/, { expected: "a tag template holding {version}" })).annotate({
167
+ description: "The tag template, with {version} for the installed version"
168
+ }),
169
+ path: Schema3.optionalKey(FilePath.annotate({ description: "The manifest inside the clone holding the version; package.json when absent" }))
170
+ }).annotate({ identifier: "Library" });
161
171
  var Sources = Schema3.Struct({
162
172
  production: Schema3.optionalKey(Schema3.Array(PathGlob).annotate({ description: "The source the repository ships, as against tests and tooling" })),
173
+ libraries: Schema3.optionalKey(Schema3.Array(Library).check(Schema3.makeFilter((libraries) => {
174
+ const repeated = duplicates(libraries.map((library) => library.name));
175
+ return repeated === undefined || `names ${repeated} more than once`;
176
+ })).annotate({ description: "The libraries checks-vendor pins to a shared read-only clone" })),
163
177
  effect: Schema3.optionalKey(EffectSources)
164
178
  });
165
179
  var Feature = Schema3.Struct({
@@ -179,11 +193,14 @@ var Feature = Schema3.Struct({
179
193
  function nests(outer, inner) {
180
194
  return outer === inner || inner.startsWith(`${outer}/`);
181
195
  }
182
- var Features = Schema3.Array(Feature).check(Schema3.makeFilter((features) => {
183
- const names = features.map((feature) => feature.name);
196
+ function duplicates(names) {
184
197
  const repeated = names.filter((name, index) => names.indexOf(name) !== index);
185
- if (repeated.length > 0)
186
- return `names ${[...new Set(repeated)].join(", ")} more than once`;
198
+ return repeated.length === 0 ? undefined : [...new Set(repeated)].join(", ");
199
+ }
200
+ var Features = Schema3.Array(Feature).check(Schema3.makeFilter((features) => {
201
+ const repeated = duplicates(features.map((feature) => feature.name));
202
+ if (repeated !== undefined)
203
+ return `names ${repeated} more than once`;
187
204
  for (const outer of features) {
188
205
  const inner = features.find((other) => other !== outer && nests(outer.root, other.root));
189
206
  if (inner !== undefined)
@@ -10,24 +10,8 @@ It arrives with the kit, since `@commitlint/cli` and `@commitlint/config-convent
10
10
  ## Workflow
11
11
 
12
12
  The lint runs in CI on pull requests, because `jj` never fires a git hook.
13
- A repository adds this workflow:
14
-
15
- ```yaml
16
- on:
17
- pull_request:
18
- types: [opened, edited, synchronize, reopened]
19
- jobs:
20
- commitlint:
21
- runs-on: ubuntu-latest
22
- steps:
23
- - uses: actions/checkout@v5
24
- - uses: oven-sh/setup-bun@v2
25
- - run: bun install --frozen-lockfile
26
- - run: printf '%s' "$PR_TITLE (#0000)" > "$RUNNER_TEMP/pr-title"
27
- env:
28
- PR_TITLE: ${{ github.event.pull_request.title }}
29
- - run: ./node_modules/.bin/commitlint --config ./node_modules/@avi2dg/checks/commitlint.config.js --edit "$RUNNER_TEMP/pr-title"
30
- ```
13
+ `checks-quality generate` writes the workflow whole into `.github/workflows/commitlint.yml`, as [checks-quality](../gates/checks-quality.md) says.
14
+ The workflow lints with the installed kit's `commitlint.config.js`, so every repository holds titles to the same rules.
31
15
 
32
16
  ## What it lints
33
17
 
@@ -39,13 +39,14 @@ The kit's bins find the file at the git root and read it there:
39
39
 
40
40
  | Key | Read by | Holds |
41
41
  | --- | --- | --- |
42
- | `defaultBranch` | `checks-lint`, `checks-ci-wiring` | the branch pull requests merge into, `main` when absent |
43
- | `gates.ci` | `checks-ci-wiring` | the commands CI runs on every pull request, as [checks-ci-wiring](../gates/checks-ci-wiring.md) says |
42
+ | `defaultBranch` | `checks-lint`, `checks-ci-wiring`, `checks-quality` | the branch pull requests merge into, `main` when absent |
43
+ | `gates.ci` | `checks-ci-wiring`, `checks-quality` | the commands CI runs on every pull request, as [checks-ci-wiring](../gates/checks-ci-wiring.md) says |
44
44
  | `gates.scheduled` | `checks-ci-wiring` | the commands a schedule runs |
45
45
  | `gates.lint` | `checks-lint`, `checks-ci-wiring` | the gates `checks-lint` runs when not all apply, as [Gate selection](../gates/checks-lint.md#gate-selection) says |
46
46
  | `commitIdentity.authors` | `checks-commit-identity` | the identities allowed to author and commit, as [checks-commit-identity](../gates/checks-commit-identity.md) says |
47
47
  | `sources.production` | `checks-size-budget`, `checks-repetition`, `checks-quality` | the source the repository ships, as [checks-size-budget](../gates/checks-size-budget.md) and [checks-repetition](../gates/checks-repetition.md) say |
48
48
  | `sources.effect` | `checks-quality` | the paths held to the Effect rules, and the files under them that are not, as [The Effect rules](effect-rules.md) says |
49
+ | `sources.libraries` | `checks-vendor`, `checks-test-layout` | the libraries pinned to a shared read-only clone, as [checks-vendor](../gates/checks-vendor.md) says |
49
50
  | `size` | `checks-size-budget` | the size budget of production and test files, and how a change is held to it, as [checks-size-budget](../gates/checks-size-budget.md) says |
50
51
  | `features` | `featureRules`, `checks-feature-owners` | each feature's root, entries, exempt importers and proof, as [checks-feature-owners](../gates/checks-feature-owners.md) says |
51
52
  | `changeSignal` | `checks-feature-owners` | `advisory` to list the feature owners a change touches |
@@ -64,7 +64,7 @@ It prints the range, the declared selection if there is one, each gate's own rep
64
64
  ```
65
65
  checks-lint: range 2504acf098d120e73a8ece3c96f22b934f35c6a8..10ba7d8935b73ed72624120a1542e51bd21ca7c7 from HEAD against origin/main
66
66
  ...
67
- checks-lint: 3 of 11 gate(s) failed: checks-commit-identity, checks-comment-gate, checks-suppressions-ratchet
67
+ checks-lint: 3 of 12 gate(s) failed: checks-commit-identity, checks-comment-gate, checks-suppressions-ratchet
68
68
  ```
69
69
 
70
70
  ## Opting out
@@ -86,6 +86,7 @@ It declares the gates `checks-lint` runs as `gates.lint`:
86
86
  "checks-suppressions-ratchet",
87
87
  "checks-ci-wiring",
88
88
  "checks-docs",
89
+ "checks-quarantine-clock",
89
90
  "checks-quality"
90
91
  ]
91
92
  }
@@ -1,12 +1,22 @@
1
1
  # checks-mutation-compare
2
2
 
3
- `checks-mutation-compare` is the gate that holds a pull request's mutation score to no regression rather than an absolute threshold, and a reader looks it up to wire mutation testing into CI.
3
+ `checks-mutation-compare` is the gate that holds every mutant in a pull request to no regression rather than an absolute score, and a reader looks it up to wire mutation testing into CI.
4
4
 
5
5
  ## What it checks
6
6
 
7
- The head mutation score may not fall below the score at the merge-base.
8
- The score is Stryker's, `Killed` and `Timeout` over those plus `Survived` and `NoCoverage`, so `CompileError`, `RuntimeError`, `Ignored` and `Pending` mutants leave it.
9
- Every file in a report counts toward that report's score, including files present in only one of the two.
7
+ A mutant regresses when it is `Killed` or `Timeout` at the base and `Survived` or `NoCoverage` at the head.
8
+ The gate matches mutants between the two reports and judges each match, so a lost kill cannot hide behind mutants that move into the score and a mutant leaving the score cannot manufacture a false regression.
9
+ A matched mutant that moves into or out of `RuntimeError`, `CompileError`, `Ignored` or `Pending` is reported apart from a regression, because that move only changes which mutants leave the score.
10
+ A mutant present in only one report is listed and never fails the comparison.
11
+
12
+ ## How it matches mutants
13
+
14
+ The gate aligns each file's base and head source, which it reads from the report's copy of the file, by a line diff.
15
+ It maps each base mutant to the place its lines moved to in the head, so a line added or removed above a mutant leaves it matched.
16
+ It then matches a mutant in the base report to a mutant in the head report by its file, that mapped location, its mutator and its replacement.
17
+ Two mutants in the same report can share all four, so the gate also counts the position of each mutant among others with the same four, in the order the report lists them, and adds that position to the key.
18
+ A mutant on a line the pull request added, removed or changed has no counterpart in the other report and lands in the unmatched list.
19
+ Each list prints in order of file, then line, then column, and a matched mutant prints at its place in the head.
10
20
 
11
21
  ## What it reads
12
22
 
@@ -37,19 +47,18 @@ checks-mutation-compare [--advisory] <base-report> <head-report>
37
47
 
38
48
  | Code | When |
39
49
  | --- | --- |
40
- | 0 | the head score is not below the base score, or `--advisory` is given |
41
- | 1 | the head score is below the base score |
50
+ | 0 | no mutant regressed, or `--advisory` is given |
51
+ | 1 | a mutant regressed |
42
52
  | 2 | a report is not a Stryker mutation report, or the arguments do not parse |
43
53
 
44
54
  ## Sample output
45
55
 
46
- It prints the overall score of each report and the per-file scores that differ:
56
+ It prints the verdict first, then each regression, each move into or out of a status that leaves the score, and each unmatched mutant:
47
57
 
48
58
  ```
49
- mutation-compare: base 83.33% (5/6) head 66.67% (4/6) delta -16.67pp
50
- src/billing.ts: 75.00% (3/4) -> 50.00% (2/4)
51
- 1 unchanged file(s)
52
- mutation-compare: REGRESSION (-16.67pp)
59
+ mutation-compare: REGRESSION (1 mutant(s))
60
+ regression src/billing.ts:12:5 ConditionalExpression "true": Killed -> Survived
61
+ moved src/loader.ts:4:1 ClassDeclaration "class {}": Killed -> RuntimeError
53
62
  ```
54
63
 
55
64
  ## Opting out
@@ -1,6 +1,6 @@
1
1
  # checks-quality
2
2
 
3
- `checks-quality` is the bin that writes what `quality.json` declares for oxlint and tsc into generated fragments and checks them, and a reader looks it up when a fragment is stale.
3
+ `checks-quality` is the bin that writes what `quality.json` declares into generated fragments and workflows and checks them, and a reader looks it up when generated text is stale.
4
4
 
5
5
  ## What it checks
6
6
 
@@ -29,11 +29,20 @@ A rule only this repository needs stays in its own `.oxlintrc.json`, whose overr
29
29
  }
30
30
  ```
31
31
 
32
+ GitHub Actions reads its own YAML and nothing else, so `checks-quality generate` also writes the kit recipe workflows whole.
33
+ `.github/workflows/ci.yml` runs every `gates.ci` command but the title lint as its own step after a frozen install.
34
+ `.github/workflows/commitlint.yml` lints the pull request title with the installed kit config.
35
+ A step one repository alone needs lives in another workflow file, never in the recipe.
36
+ All generated workflows are committed.
37
+
32
38
  `checks-quality --check` fails when any of these holds:
33
39
 
34
40
  - A fragment is missing, or differs from what `generate` would write from `quality.json` and the installed kit's presets.
35
41
  - A fragment is left over once `quality.json` stops declaring `sources.effect`.
42
+ - A generated workflow is missing, or differs from what `generate` would write from `quality.json` and the kit recipe.
36
43
  - `.oxlintrc.json` or `tsconfig.json` does not list its fragment in `extends`, so the tool never reads it.
44
+ - `.oxlintrc.json` does not extend `./node_modules/@avi2dg/checks/oxlintrc.json`, so the kit's oxlint rules are not loaded.
45
+ - `tsconfig.json` does not extend `@avi2dg/checks/tsconfig.effect.json`, the one accepted spelling of the kit's Effect config.
37
46
  - A `sources.effect.paths` or `sources.production` glob matches no tracked or untracked file, so it holds nothing.
38
47
 
39
48
  Two details of the fragments are easy to get wrong, so the kit's tests pin both:
@@ -47,7 +56,10 @@ Two details of the fragments are easy to get wrong, so the kit's tests pin both:
47
56
 
48
57
  ## What it reads
49
58
 
50
- It reads the working tree: `quality.json`, the presets of the installed kit, `.oxlintrc.json`, `tsconfig.json` and the two fragments.
59
+ It reads the working tree: `quality.json`, the presets of the installed kit, `.oxlintrc.json`, `tsconfig.json`, the two fragments and the generated workflows.
60
+ It reads the root `package.json` name, since only the kit's own tree lints titles with its root `commitlint.config.js`.
61
+ The name also decides which kit configs `extends` must list, since the kit's own tree extends its root `oxlintrc.json` and `tsconfig.effect.json`.
62
+ It looks for `.bun-version`, and the suite pins its bun to that file when the file exists.
51
63
  It lists the tracked and untracked files to see what each declared glob matches.
52
64
 
53
65
  ## Arguments
@@ -57,15 +69,15 @@ checks-quality generate
57
69
  checks-quality --check
58
70
  ```
59
71
 
60
- `generate` writes the fragments, removes a left-over one, then runs the same check as `--check`.
72
+ `generate` writes the fragments and the workflows, removes a left-over one, then runs the same check as `--check`.
61
73
  `--check` writes nothing.
62
74
 
63
75
  ## Exit codes
64
76
 
65
77
  | Code | When |
66
78
  | --- | --- |
67
- | 0 | the fragments hold what `quality.json` declares |
68
- | 1 | a fragment is stale, missing, left over or not extended, or a declared glob matches no file |
79
+ | 0 | the generated files hold what `quality.json` declares |
80
+ | 1 | a generated file is stale, missing or left over, a fragment is not extended, the kit's config is not extended, or a declared glob matches no file |
69
81
  | 2 | `quality.json` does not decode, or the arguments are neither `generate` nor `--check` |
70
82
 
71
83
  ## Sample output
@@ -76,16 +88,19 @@ checks-quality: 2 problem(s) with what quality.json declares:
76
88
  tsconfig.json does not extend ./tsconfig.quality.json, so the language service never reads it
77
89
  ```
78
90
 
79
- A passing run says what the fragments hold:
91
+ A passing run says what the generated files hold:
80
92
 
81
93
  ```
82
- checks-quality: oxlintrc.quality.json and tsconfig.quality.json hold what quality.json declares
94
+ checks-quality: oxlintrc.quality.json and tsconfig.quality.json and .github/workflows/ci.yml and .github/workflows/commitlint.yml hold what quality.json declares
83
95
  ```
84
96
 
97
+ A stale workflow is reported the same way as a stale fragment.
98
+
85
99
  ## Opting out
86
100
 
87
101
  It runs only in a repository that tracks `quality.json`, and a selection in `quality.json` always keeps it, since the file it sits in is what makes it apply.
88
- A repository that declares no `sources.effect` gets no fragment, and the check then only refuses a left-over one.
102
+ A repository that declares no `sources.effect` gets no fragment, and the check then refuses a left-over one.
103
+ Every repository gets the title lint workflow, and one with `gates.ci` gets the suite with it.
89
104
 
90
105
  ## Related topics
91
106
 
@@ -0,0 +1,54 @@
1
+ # checks-quarantine-clock
2
+
3
+ `checks-quarantine-clock` is the gate that fails a test left in `tests/quarantine/` past 30 days, and a reader looks it up when a quarantined test went red.
4
+
5
+ ## What it checks
6
+
7
+ It fails naming each test that entered `tests/quarantine/` more than 30 days before the head.
8
+ `checks-test-layout` pins `tests/quarantine/` out of every default run, so a test there protects nothing until it moves back.
9
+ Each failure names the file, the day it entered quarantine, and what to do, which is to fix it and move it back, or delete it.
10
+ The limit is 30 days for every test, with no setting to raise it.
11
+ GitLab quarantines fast for 3 days and long term for at most 3 months, then opens a deletion merge request automatically.
12
+
13
+ ## What it reads
14
+
15
+ It lists the test files under `tests/quarantine/` at the head, by the name pattern `checks-test-layout` uses, so a helper or fixture there never ages out.
16
+ It walks each file's history with `git log --follow`, so a move or a copy into quarantine starts the clock there rather than at the test's creation.
17
+ It measures the age from the author date of the commit that put the file there to the later of the head's author and committer dates, so the same commit always gets the same verdict.
18
+ A rebase keeps the entry's author date and moves the head's committer date forward, so rebasing neither restarts the clock nor stops it.
19
+
20
+ ## Arguments
21
+
22
+ ```sh
23
+ checks-quarantine-clock <base-ref> <head-ref>
24
+ checks-quarantine-clock <ref>
25
+ ```
26
+
27
+ With two arguments it judges the head, and the base only satisfies the range `checks-lint` hands every range gate.
28
+ With one it judges that commit, including a repository's first commit.
29
+
30
+ ## Exit codes
31
+
32
+ | Code | When |
33
+ | --- | --- |
34
+ | 0 | no test in `tests/quarantine/` is past 30 days |
35
+ | 1 | a test in `tests/quarantine/` is past 30 days |
36
+ | 2 | a ref does not resolve, or the history ends before a file's entry into quarantine, as in a shallow clone |
37
+
38
+ ## Sample output
39
+
40
+ ```
41
+ quarantine-clock: 1 test(s) in tests/quarantine/ is past 30 days; fix each and move it back, or delete it:
42
+ tests/quarantine/billing.test.ts entered quarantine on 2026-08-01 (45 days ago)
43
+ ```
44
+
45
+ ## Opting out
46
+
47
+ It applies to every repository, so no selection leaves it out.
48
+ A repository with no test file under `tests/quarantine/` passes with nothing checked.
49
+ `checks-lint` runs it over each pull request's range, as [checks-lint](checks-lint.md) says.
50
+
51
+ ## Related topics
52
+
53
+ - [checks-test-layout](checks-test-layout.md)
54
+ - [checks-lint](checks-lint.md)
@@ -18,19 +18,23 @@ It fails unless the repository holds this shape, and names the file and the path
18
18
  - `scripts.test` is exactly `checks-test`, which runs `bun test --randomize` as [checks-test](checks-test.md) says.
19
19
  - `scripts.lint` runs this check, itself or through `checks-lint` called by its bare bin name.
20
20
  - `bunfig.toml` carries every `[test]` key of the shipped preset with the same value.
21
- `[test].pathIgnorePatterns` is always `["**/tests/quarantine/**"]`, which the check pins itself, so the kit's own repository, whose bunfig is the preset, cannot drift it either.
21
+ `[test].pathIgnorePatterns` is the preset's `["**/tests/quarantine/**", "repos/**"]`, which the check pins itself, so the kit's own repository, whose bunfig is the preset, cannot drift it either.
22
+ A repository whose `quality.json` declares no `sources.libraries` may hold `["**/tests/quarantine/**"]` instead, and one that declares them must keep `repos/**`.
22
23
  Other tables, and extra `[test]` keys, are the repository's own.
23
24
 
24
25
  The in-process half is what a mutation run can mutate.
25
26
  `tests/e2e/**` is left out of a mutate scope by construction, because a subprocess kills both the speed and the coverage signal a mutant needs.
26
27
 
27
- The preset also skips `tests/quarantine/**` on a default run.
28
+ The preset also skips `tests/quarantine/**` and `repos/**` on a default run.
29
+ The `repos/**` entry keeps the suite from following the library links `checks-vendor` manages into trees whose tests are not this repository's.
28
30
  A test that turns flaky moves there, so the suite stays trustworthy, and the flake still runs on demand:
29
31
 
30
32
  ```sh
31
33
  bun test --path-ignore-patterns='' tests/quarantine
32
34
  ```
33
35
 
36
+ A test left there past 30 days fails [checks-quarantine-clock](checks-quarantine-clock.md).
37
+
34
38
  ## What it reads
35
39
 
36
40
  It reads the working tree.
@@ -38,6 +42,7 @@ It scans the tracked and untracked files that `git ls-files --exclude-standard`
38
42
  It parses each test and helper with swc and reads import specifiers and identifier use, so a test that only carries `"node:child_process"` as a string is not a violation.
39
43
  `tests/fixtures/**` is data and is not parsed.
40
44
  It reads `package.json` for `scripts.test` and `scripts.lint`, and compares `bunfig.toml` with the preset the installed kit ships.
45
+ It reads `quality.json` for `sources.libraries`, which decides whether `repos/**` is pinned.
41
46
 
42
47
  ## Arguments
43
48
 
@@ -53,7 +58,7 @@ It checks the directory it runs in, or the directory it is given.
53
58
  | --- | --- |
54
59
  | 0 | the repository holds the layout |
55
60
  | 1 | a file breaks the layout |
56
- | 2 | a test, a helper or `package.json` does not parse |
61
+ | 2 | a test, a helper, `package.json` or `quality.json` does not parse |
57
62
 
58
63
  ## Sample output
59
64
 
@@ -75,3 +80,4 @@ A repository that tracks one keeps it.
75
80
  - [checks-test](checks-test.md)
76
81
  - [checks-flake](checks-flake.md)
77
82
  - [checks-mutation-compare](checks-mutation-compare.md)
83
+ - [checks-quarantine-clock](checks-quarantine-clock.md)
@@ -0,0 +1,97 @@
1
+ # checks-vendor
2
+
3
+ `checks-vendor` pins each library `quality.json` declares to one shared read-only clone on the machine and links it under `repos/`.
4
+
5
+ ## What it checks
6
+
7
+ Each entry under `sources.libraries` names an npm package, a git remote and a tag template holding `{version}`.
8
+ Its `name` is the link under `repos/`, and its optional `path` is the manifest inside the clone that holds the version, `package.json` when absent.
9
+ `checks-vendor` reads the installed version from `node_modules/<package>/package.json` and resolves the template to one tag.
10
+ It clones that tag once into a cache shared across repositories, records the landed commit in `<tag>.commit` beside the tree, strips every write bit and links `repos/<name>` to the tree.
11
+ The clone is staged beside its cache entry and moves into place only once it is recorded, checked and read only, so a concurrent or killed first run never leaves a half built tree there.
12
+ Every later run verifies the link rather than trusting it, and does so without contacting the remote.
13
+ It confirms the tree sits on the recorded commit.
14
+ It confirms the manifest inside the tree still names the installed version.
15
+ It confirms no write bit came back and no write landed outside the recorded commit.
16
+ It never follows a link inside the tree, so no mode outside the cache is touched.
17
+ Any failed confirmation fails the run.
18
+ A failed library drops its `repos/<name>` link, so a reader falls back to `node_modules/<package>` rather than a tree the run could not vouch for.
19
+ A missing tag, an unknown installed version or a manifest naming another version fails it too.
20
+ A tag moved upstream after the first fetch is not followed, since a cached tree stays on its recorded commit.
21
+ Clearing the tree with `chmod -R u+w <dir> && rm -rf <dir>` keeps the record, so the next fetch fails when the tag now lands elsewhere.
22
+ A deliberate move also deletes `<tag>.commit` by hand before `checks-vendor` runs again.
23
+ The cache lives at `~/.cache/avi2dg-checks/repos/<host>/<owner>/<repo>/<tag>/`.
24
+ A read through the link resolves outside the checkout, so a reader that must stay inside the tree falls back to `node_modules/<package>`.
25
+
26
+ ## What it reads
27
+
28
+ It reads the working tree.
29
+ That is `quality.json`, `node_modules/<package>/package.json` for each declared library and the `repos/` links.
30
+ It reads the shared cache outside the checkout.
31
+ That is each tag tree, its recorded commit and its manifest.
32
+ It contacts a remote only when a tag is not cached yet, to confirm the tag exists and to clone it.
33
+
34
+ ## Arguments
35
+
36
+ ```sh
37
+ checks-vendor
38
+ ```
39
+
40
+ It takes no arguments, since `quality.json` names the libraries.
41
+
42
+ ## Exit codes
43
+
44
+ | Code | When |
45
+ | --- | --- |
46
+ | 0 | every declared library links a verified tree, its remote could not be reached for a first fetch, or no git checkout holds the run |
47
+ | 1 | a tag is missing or lands elsewhere than its record, an installed version is unknown, a tree was written to, a record disagrees with its tree, a version disagrees or a link is blocked |
48
+ | 2 | `quality.json` does not decode, `HOME` is unset, or arguments were passed |
49
+
50
+ ## Sample output
51
+
52
+ ```
53
+ checks-vendor: cloned effect@4.0.0-rc.115 from https://github.com/Effect-TS/effect.git and linked repos/effect
54
+ ```
55
+
56
+ A fresh fetch reports the tag it cloned and the link it made.
57
+
58
+ ```
59
+ checks-vendor: repos/effect still holds effect@4.0.0-rc.115, verified against its recorded commit
60
+ ```
61
+
62
+ A later run reports the link it kept.
63
+
64
+ ## Wiring
65
+
66
+ A consuming repository runs it from its `prepare` script, so every install pins and verifies the trees.
67
+
68
+ ```json
69
+ { "scripts": { "prepare": "checks-vendor" } }
70
+ ```
71
+
72
+ An install offline still passes.
73
+ A cached tree verifies with no network, and a tag that is not cached yet warns, stays unlinked and leaves readers on `node_modules/<package>` until a later install can fetch it.
74
+ An install outside a git checkout, or with no `git` on the path, warns once, links nothing and passes too.
75
+
76
+ It ignores the links in `.gitignore` with `repos/*`, because `repos/*/` does not match a link.
77
+
78
+ ```gitignore
79
+ repos/*
80
+ ```
81
+
82
+ TypeScript does not read `.gitignore`, and its default `include` follows the links into each library tree.
83
+ A consumer whose `tsconfig.json` has no explicit `include` keeps the trees out with an `exclude` entry.
84
+
85
+ ```json
86
+ { "exclude": ["node_modules", "repos"] }
87
+ ```
88
+
89
+ ## Opting out
90
+
91
+ It runs only in a repository that declares `sources.libraries`.
92
+ A repository without that key reports nothing to pin and changes nothing.
93
+ A repository that declares no library needs no `prepare` entry for it.
94
+
95
+ ## Related topics
96
+
97
+ - [The quality file](../configs/quality-file.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avi2dg/checks",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "description": "Deterministic checks shared across the captain's TypeScript repos",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -32,6 +32,7 @@
32
32
  "scripts/commit-identity.ts",
33
33
  "scripts/mutation-compare.ts",
34
34
  "scripts/ci-wiring.ts",
35
+ "scripts/shell-command.ts",
35
36
  "scripts/comment-matchers.ts",
36
37
  "scripts/prose-matchers.ts",
37
38
  "scripts/comments.ts",
@@ -41,10 +42,13 @@
41
42
  "scripts/quality-file.ts",
42
43
  "scripts/quality.ts",
43
44
  "scripts/size-budget.ts",
45
+ "scripts/range-gate.ts",
44
46
  "scripts/size-rules.ts",
45
47
  "scripts/repetition.ts",
46
48
  "scripts/feature-owners.ts",
49
+ "scripts/quarantine-clock.ts",
47
50
  "scripts/docs.ts",
51
+ "scripts/vendor.ts",
48
52
  "scripts/doc-outline.ts",
49
53
  "scripts/doc-rules.ts",
50
54
  "scripts/doc-references.ts",
@@ -83,7 +87,9 @@
83
87
  "./scripts/size-budget.ts": "./scripts/size-budget.ts",
84
88
  "./scripts/repetition.ts": "./scripts/repetition.ts",
85
89
  "./scripts/feature-owners.ts": "./scripts/feature-owners.ts",
90
+ "./scripts/quarantine-clock.ts": "./scripts/quarantine-clock.ts",
86
91
  "./scripts/docs.ts": "./scripts/docs.ts",
92
+ "./scripts/vendor.ts": "./scripts/vendor.ts",
87
93
  "./templates/readme.md": "./templates/readme.md",
88
94
  "./templates/changelog.md": "./templates/changelog.md",
89
95
  "./templates/adr.md": "./templates/adr.md",
@@ -118,9 +124,12 @@
118
124
  "checks-size-budget": "scripts/size-budget.ts",
119
125
  "checks-repetition": "scripts/repetition.ts",
120
126
  "checks-feature-owners": "scripts/feature-owners.ts",
121
- "checks-docs": "scripts/docs.ts"
127
+ "checks-quarantine-clock": "scripts/quarantine-clock.ts",
128
+ "checks-docs": "scripts/docs.ts",
129
+ "checks-vendor": "scripts/vendor.ts"
122
130
  },
123
131
  "scripts": {
132
+ "prepare": "bun scripts/vendor.ts",
124
133
  "build": "bun build effect-channel/index.ts --outdir dist --target node --format esm && bun build scripts/feature-rules.ts --outdir dist --target node --format esm --packages external && bun scripts/quality-schema.ts && bun scripts/doc-templates-write.ts && bun scripts/changelog-write.ts && bun scripts/doc-blocks-write.ts",
125
134
  "lint": "oxlint --type-aware && bun scripts/lint.ts && depcruise --config .dependency-cruiser.cjs .",
126
135
  "typecheck": "tsc --noEmit && effect-tsgo diagnostics --project tsconfig.json --format text --strict",