@avi2dg/checks 0.18.0 → 0.20.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/CHANGELOG.md CHANGED
@@ -2,6 +2,29 @@
2
2
 
3
3
  Every release of `@avi2dg/checks`, newest first, written by the release from its conventional commits.
4
4
 
5
+ ## 0.20.0
6
+
7
+ Released 2026-09-26.
8
+
9
+ ### Features
10
+
11
+ - **scripts:** pin node from .node-version in the generated CI workflow (#64)
12
+ - **scripts:** add checks-subsumed-tests to report tests another test subsumes in a (#62)
13
+
14
+ ### Fixes
15
+
16
+ - **scripts:** lint a PR title that starts with # in the generated commitlint workflow (#63)
17
+
18
+ ## 0.19.0
19
+
20
+ Released 2026-09-26.
21
+
22
+ ### Features
23
+
24
+ - **scripts:** judge checks-mutation-compare mutant by mutant instead of by score (#58)
25
+ - **scripts:** fail a test left in tests/quarantine past 30 days (#59)
26
+ - **scripts:** pin shared read-only library clones with checks-vendor (#57)
27
+
5
28
  ## 0.18.0
6
29
 
7
30
  Released 2026-09-26.
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,10 @@ 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.
135
+ - [`checks-subsumed-tests`](docs/gates/checks-subsumed-tests.md) lists each test another test subsumes in a mutation run.
134
136
  - [`checks-backtest`](docs/gates/checks-backtest.md) reports what the comment check would have refused in recent history.
137
+ - [`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
138
 
136
139
  `checks-lint` has [its own page](docs/gates/checks-lint.md), which says which range it resolves.
137
140
  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)
@@ -18,6 +18,7 @@ The workflow lints with the installed kit's `commitlint.config.js`, so every rep
18
18
  It lints the pull request title and nothing else.
19
19
  The title is the enforced subject because a squash merge uses it as the main commit subject, and per-commit messages are not linted.
20
20
  GitHub appends ` (#N)` to the squashed subject, so the workflow lints the title with that suffix attached, and the header length limit applies to the landed subject, not the bare title.
21
+ The workflow moves git's comment character off `#`, so a title starting with `#` is linted like any other.
21
22
 
22
23
  It never sees a commit's author or committer fields, nor the `Co-authored-by` trailer GitHub writes from a foreign author when it squashes, so it cannot enforce who a commit belongs to.
23
24
  [checks-commit-identity](../gates/checks-commit-identity.md) is that enforcement.
@@ -46,6 +46,7 @@ The kit's bins find the file at the git root and read it there:
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
@@ -59,7 +59,7 @@ Two details of the fragments are easy to get wrong, so the kit's tests pin both:
59
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
60
  It reads the root `package.json` name, since only the kit's own tree lints titles with its root `commitlint.config.js`.
61
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.
62
+ It looks for `.bun-version` and `.node-version`, and the suite pins its bun and its node to whichever of the two files exists.
63
63
  It lists the tracked and untracked files to see what each declared glob matches.
64
64
 
65
65
  ## Arguments
@@ -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)
@@ -0,0 +1,68 @@
1
+ # checks-subsumed-tests
2
+
3
+ `checks-subsumed-tests` is the report that lists each test another test subsumes in a Stryker mutation run, and a reader looks it up to judge whether the suite carries tests it no longer needs.
4
+
5
+ ## What it checks
6
+
7
+ It checks nothing and fails nothing.
8
+ It reads one Stryker `mutation.json` report, gives each test the set of mutants it kills, and prints each test whose kill set sits inside the kill set of one other test beside that test, with both kill counts.
9
+ It then prints the tests whose kill sets are identical, then the size of a greedy cover that keeps every kill out of every test the report lists.
10
+ A test is subsumed when its kill set sits inside the kill set of one other test, so dropping every subsumed test loses no kill in this run.
11
+ The report informs a person and decides nothing, so keep a subsumed test unless reading the pair shows the same scenario.
12
+ A subsumed verdict trusts the mutants the run covers, so read the files the report names before judging a test redundant.
13
+ A test judged against a module that is not its subject looks redundant until its own subject is mutated.
14
+ A test whose subject no mutant can touch, such as frontmatter or links, looks redundant because mutation cannot see what it checks.
15
+
16
+ ## What it reads
17
+
18
+ It reads one Stryker `mutation.json` report built with bail off, which the shared Stryker preset's `json` reporter writes to `reports/mutation/mutation.json`.
19
+ With `disableBail` Stryker runs every covering test for each mutant and records every test that fails as a killer, so each test gets a kill set.
20
+ A report built with bail on records one killer per mutant, which makes every test look unique.
21
+ Stryker writes its options into the report's `config`, so `checks-subsumed-tests` refuses a report whose `config.disableBail` is not `true`.
22
+ A report with no `config` shows nothing about bail, so `checks-subsumed-tests` prints one warning and reads it anyway.
23
+ Build a bail-off report with this command:
24
+
25
+ ```sh
26
+ bunx stryker run --disableBail
27
+ ```
28
+
29
+ The report records each killer as a test index, so it names each test by its file and its name from the report's `testFiles` table.
30
+
31
+ ## Arguments
32
+
33
+ ```sh
34
+ checks-subsumed-tests <mutation-report>
35
+ ```
36
+
37
+ ## Exit codes
38
+
39
+ | Code | When |
40
+ | --- | --- |
41
+ | 0 | the report printed |
42
+ | 2 | the report is not a Stryker mutation report, it was built with bail on, or the arguments do not parse |
43
+
44
+ ## Sample output
45
+
46
+ It prints the files the run mutated, then each subsumed test beside the test that subsumes it, then the tests whose kill sets are identical, then the greedy cover:
47
+
48
+ ```
49
+ subsumed-tests: 2 file(s) mutated
50
+ mutated src/add.ts
51
+ mutated src/mul.ts
52
+ subsumed tests (1):
53
+ "tests/add.test.ts > add sums two numbers" (1 kill) subsumed by "tests/add.test.ts > add covers every operator" (3 kills)
54
+ identical kill sets (1 group(s)):
55
+ "tests/mul.test.ts > mul multiplies" (2 kills) = "tests/mul.test.ts > mul multiplies in either order" (2 kills)
56
+ greedy cover: 3 of 6 test(s) keep all 6 kill(s)
57
+ cover "tests/add.test.ts > add covers every operator"
58
+ cover "tests/mul.test.ts > mul multiplies"
59
+ cover "tests/mul.test.ts > mul checks its guard"
60
+ ```
61
+
62
+ ## Opting out
63
+
64
+ Nothing runs it but a person who wants the figures.
65
+
66
+ ## Related topics
67
+
68
+ - [checks-mutation-compare](checks-mutation-compare.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.18.0",
3
+ "version": "0.20.0",
4
4
  "description": "Deterministic checks shared across the captain's TypeScript repos",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -31,6 +31,7 @@
31
31
  "scripts/flake.ts",
32
32
  "scripts/commit-identity.ts",
33
33
  "scripts/mutation-compare.ts",
34
+ "scripts/subsumed-tests.ts",
34
35
  "scripts/ci-wiring.ts",
35
36
  "scripts/shell-command.ts",
36
37
  "scripts/comment-matchers.ts",
@@ -46,7 +47,9 @@
46
47
  "scripts/size-rules.ts",
47
48
  "scripts/repetition.ts",
48
49
  "scripts/feature-owners.ts",
50
+ "scripts/quarantine-clock.ts",
49
51
  "scripts/docs.ts",
52
+ "scripts/vendor.ts",
50
53
  "scripts/doc-outline.ts",
51
54
  "scripts/doc-rules.ts",
52
55
  "scripts/doc-references.ts",
@@ -73,6 +76,7 @@
73
76
  "./scripts/flake.ts": "./scripts/flake.ts",
74
77
  "./scripts/commit-identity.ts": "./scripts/commit-identity.ts",
75
78
  "./scripts/mutation-compare.ts": "./scripts/mutation-compare.ts",
79
+ "./scripts/subsumed-tests.ts": "./scripts/subsumed-tests.ts",
76
80
  "./scripts/ci-wiring.ts": "./scripts/ci-wiring.ts",
77
81
  "./scripts/comment-matchers.ts": "./scripts/comment-matchers.ts",
78
82
  "./scripts/prose-matchers.ts": "./scripts/prose-matchers.ts",
@@ -85,7 +89,9 @@
85
89
  "./scripts/size-budget.ts": "./scripts/size-budget.ts",
86
90
  "./scripts/repetition.ts": "./scripts/repetition.ts",
87
91
  "./scripts/feature-owners.ts": "./scripts/feature-owners.ts",
92
+ "./scripts/quarantine-clock.ts": "./scripts/quarantine-clock.ts",
88
93
  "./scripts/docs.ts": "./scripts/docs.ts",
94
+ "./scripts/vendor.ts": "./scripts/vendor.ts",
89
95
  "./templates/readme.md": "./templates/readme.md",
90
96
  "./templates/changelog.md": "./templates/changelog.md",
91
97
  "./templates/adr.md": "./templates/adr.md",
@@ -112,6 +118,7 @@
112
118
  "checks-flake": "scripts/flake.ts",
113
119
  "checks-commit-identity": "scripts/commit-identity.ts",
114
120
  "checks-mutation-compare": "scripts/mutation-compare.ts",
121
+ "checks-subsumed-tests": "scripts/subsumed-tests.ts",
115
122
  "checks-ci-wiring": "scripts/ci-wiring.ts",
116
123
  "checks-comment-gate": "scripts/comment-gate.ts",
117
124
  "checks-suppressions-ratchet": "scripts/suppressions-ratchet.ts",
@@ -120,9 +127,12 @@
120
127
  "checks-size-budget": "scripts/size-budget.ts",
121
128
  "checks-repetition": "scripts/repetition.ts",
122
129
  "checks-feature-owners": "scripts/feature-owners.ts",
123
- "checks-docs": "scripts/docs.ts"
130
+ "checks-quarantine-clock": "scripts/quarantine-clock.ts",
131
+ "checks-docs": "scripts/docs.ts",
132
+ "checks-vendor": "scripts/vendor.ts"
124
133
  },
125
134
  "scripts": {
135
+ "prepare": "bun scripts/vendor.ts",
126
136
  "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",
127
137
  "lint": "oxlint --type-aware && bun scripts/lint.ts && depcruise --config .dependency-cruiser.cjs .",
128
138
  "typecheck": "tsc --noEmit && effect-tsgo diagnostics --project tsconfig.json --format text --strict",