@avi2dg/checks 0.18.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.
- package/CHANGELOG.md +10 -0
- package/README.md +3 -1
- package/bunfig.toml +1 -1
- package/dependency-cruiser.config.js +2 -0
- package/dist/feature-rules.js +22 -5
- package/docs/configs/quality-file.md +1 -0
- package/docs/gates/checks-lint.md +2 -1
- package/docs/gates/checks-mutation-compare.md +20 -11
- package/docs/gates/checks-quarantine-clock.md +54 -0
- package/docs/gates/checks-test-layout.md +9 -3
- package/docs/gates/checks-vendor.md +97 -0
- package/package.json +9 -2
- package/quality.schema.json +56 -4
- package/scripts/gates.ts +1 -0
- package/scripts/git.ts +10 -0
- package/scripts/lint.ts +75 -35
- package/scripts/mutation-compare.ts +179 -70
- package/scripts/quality-file.ts +30 -3
- package/scripts/quarantine-clock.ts +153 -0
- package/scripts/repetition.ts +40 -19
- package/scripts/size-budget.ts +46 -20
- package/scripts/suppressions-ratchet.ts +1 -5
- package/scripts/test-layout.ts +28 -17
- package/scripts/vendor.ts +337 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
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
|
+
|
|
5
15
|
## 0.18.0
|
|
6
16
|
|
|
7
17
|
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,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
|
|
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"],
|
package/dist/feature-rules.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
186
|
-
|
|
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)
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
8
|
-
The
|
|
9
|
-
|
|
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 |
|
|
41
|
-
| 1 |
|
|
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
|
|
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:
|
|
50
|
-
src/billing.ts:
|
|
51
|
-
1
|
|
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
|
|
@@ -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
|
|
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 `
|
|
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.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Deterministic checks shared across the captain's TypeScript repos",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -46,7 +46,9 @@
|
|
|
46
46
|
"scripts/size-rules.ts",
|
|
47
47
|
"scripts/repetition.ts",
|
|
48
48
|
"scripts/feature-owners.ts",
|
|
49
|
+
"scripts/quarantine-clock.ts",
|
|
49
50
|
"scripts/docs.ts",
|
|
51
|
+
"scripts/vendor.ts",
|
|
50
52
|
"scripts/doc-outline.ts",
|
|
51
53
|
"scripts/doc-rules.ts",
|
|
52
54
|
"scripts/doc-references.ts",
|
|
@@ -85,7 +87,9 @@
|
|
|
85
87
|
"./scripts/size-budget.ts": "./scripts/size-budget.ts",
|
|
86
88
|
"./scripts/repetition.ts": "./scripts/repetition.ts",
|
|
87
89
|
"./scripts/feature-owners.ts": "./scripts/feature-owners.ts",
|
|
90
|
+
"./scripts/quarantine-clock.ts": "./scripts/quarantine-clock.ts",
|
|
88
91
|
"./scripts/docs.ts": "./scripts/docs.ts",
|
|
92
|
+
"./scripts/vendor.ts": "./scripts/vendor.ts",
|
|
89
93
|
"./templates/readme.md": "./templates/readme.md",
|
|
90
94
|
"./templates/changelog.md": "./templates/changelog.md",
|
|
91
95
|
"./templates/adr.md": "./templates/adr.md",
|
|
@@ -120,9 +124,12 @@
|
|
|
120
124
|
"checks-size-budget": "scripts/size-budget.ts",
|
|
121
125
|
"checks-repetition": "scripts/repetition.ts",
|
|
122
126
|
"checks-feature-owners": "scripts/feature-owners.ts",
|
|
123
|
-
"checks-
|
|
127
|
+
"checks-quarantine-clock": "scripts/quarantine-clock.ts",
|
|
128
|
+
"checks-docs": "scripts/docs.ts",
|
|
129
|
+
"checks-vendor": "scripts/vendor.ts"
|
|
124
130
|
},
|
|
125
131
|
"scripts": {
|
|
132
|
+
"prepare": "bun scripts/vendor.ts",
|
|
126
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",
|
|
127
134
|
"lint": "oxlint --type-aware && bun scripts/lint.ts && depcruise --config .dependency-cruiser.cjs .",
|
|
128
135
|
"typecheck": "tsc --noEmit && effect-tsgo diagnostics --project tsconfig.json --format text --strict",
|
package/quality.schema.json
CHANGED
|
@@ -48,7 +48,8 @@
|
|
|
48
48
|
"checks-quality",
|
|
49
49
|
"checks-size-budget",
|
|
50
50
|
"checks-repetition",
|
|
51
|
-
"checks-feature-owners"
|
|
51
|
+
"checks-feature-owners",
|
|
52
|
+
"checks-quarantine-clock"
|
|
52
53
|
]
|
|
53
54
|
},
|
|
54
55
|
"allOf": [
|
|
@@ -76,6 +77,11 @@
|
|
|
76
77
|
"contains": {
|
|
77
78
|
"const": "checks-docs"
|
|
78
79
|
}
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"contains": {
|
|
83
|
+
"const": "checks-quarantine-clock"
|
|
84
|
+
}
|
|
79
85
|
}
|
|
80
86
|
]
|
|
81
87
|
}
|
|
@@ -114,6 +120,12 @@
|
|
|
114
120
|
},
|
|
115
121
|
"description": "The source the repository ships, as against tests and tooling"
|
|
116
122
|
},
|
|
123
|
+
"libraries": {
|
|
124
|
+
"type": "array",
|
|
125
|
+
"items": {
|
|
126
|
+
"$ref": "#/$defs/Library"
|
|
127
|
+
}
|
|
128
|
+
},
|
|
117
129
|
"effect": {
|
|
118
130
|
"type": "object",
|
|
119
131
|
"properties": {
|
|
@@ -237,12 +249,12 @@
|
|
|
237
249
|
"type": "array",
|
|
238
250
|
"prefixItems": [
|
|
239
251
|
{
|
|
240
|
-
"$ref": "#/$defs/
|
|
252
|
+
"$ref": "#/$defs/FilePath_1"
|
|
241
253
|
}
|
|
242
254
|
],
|
|
243
255
|
"minItems": 1,
|
|
244
256
|
"items": {
|
|
245
|
-
"$ref": "#/$defs/
|
|
257
|
+
"$ref": "#/$defs/FilePath_1"
|
|
246
258
|
},
|
|
247
259
|
"description": "The files under root that code outside it imports the feature through"
|
|
248
260
|
},
|
|
@@ -417,12 +429,52 @@
|
|
|
417
429
|
"pattern": "^(?!\\.\\.?(?:\\/|$))(?:\\*\\*|(?:[\\w.@+-]|\\*(?!\\*))+)(?:\\/(?!\\.\\.?(?:\\/|$))(?:\\*\\*|(?:[\\w.@+-]|\\*(?!\\*))+))*\\/(?:[\\w.@+-]|\\*(?!\\*))*\\.\\w+$",
|
|
418
430
|
"description": "A glob from the repository root that oxlint, the Effect language service and git read alike: a directory first, * within a segment, ** as a whole one, a file name with an extension last, and no braces, ?, [ or leading ./"
|
|
419
431
|
},
|
|
432
|
+
"FilePath": {
|
|
433
|
+
"type": "string",
|
|
434
|
+
"pattern": "^(?:(?!\\.\\.?(?:\\/|$))[\\w.@+-]+\\/)*[\\w.@+-]*\\.\\w+$",
|
|
435
|
+
"description": "The manifest inside the clone holding the version; package.json when absent"
|
|
436
|
+
},
|
|
437
|
+
"Library": {
|
|
438
|
+
"type": "object",
|
|
439
|
+
"properties": {
|
|
440
|
+
"name": {
|
|
441
|
+
"type": "string",
|
|
442
|
+
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
|
|
443
|
+
"description": "The link checks-vendor manages under repos/"
|
|
444
|
+
},
|
|
445
|
+
"package": {
|
|
446
|
+
"type": "string",
|
|
447
|
+
"minLength": 1,
|
|
448
|
+
"description": "The npm package whose installed version picks the tag"
|
|
449
|
+
},
|
|
450
|
+
"repository": {
|
|
451
|
+
"type": "string",
|
|
452
|
+
"minLength": 1,
|
|
453
|
+
"description": "The git remote checks-vendor clones the tag from"
|
|
454
|
+
},
|
|
455
|
+
"tag": {
|
|
456
|
+
"type": "string",
|
|
457
|
+
"pattern": "\\{version\\}",
|
|
458
|
+
"description": "The tag template, with {version} for the installed version"
|
|
459
|
+
},
|
|
460
|
+
"path": {
|
|
461
|
+
"$ref": "#/$defs/FilePath"
|
|
462
|
+
}
|
|
463
|
+
},
|
|
464
|
+
"required": [
|
|
465
|
+
"name",
|
|
466
|
+
"package",
|
|
467
|
+
"repository",
|
|
468
|
+
"tag"
|
|
469
|
+
],
|
|
470
|
+
"additionalProperties": false
|
|
471
|
+
},
|
|
420
472
|
"DirectoryPath": {
|
|
421
473
|
"type": "string",
|
|
422
474
|
"pattern": "^(?!\\.\\.?(?:\\/|$))[\\w.@+-]+(?:\\/(?!\\.\\.?(?:\\/|$))[\\w.@+-]+)*$",
|
|
423
475
|
"description": "The directory the feature owns"
|
|
424
476
|
},
|
|
425
|
-
"
|
|
477
|
+
"FilePath_1": {
|
|
426
478
|
"type": "string",
|
|
427
479
|
"pattern": "^(?:(?!\\.\\.?(?:\\/|$))[\\w.@+-]+\\/)*[\\w.@+-]*\\.\\w+$"
|
|
428
480
|
},
|
package/scripts/gates.ts
CHANGED
|
@@ -42,6 +42,7 @@ export const KIT_GATES = [
|
|
|
42
42
|
{ bin: "checks-size-budget", script: "size-budget.ts", reads: "range", appliesTo: TYPESCRIPT_SOURCE },
|
|
43
43
|
{ bin: "checks-repetition", script: "repetition.ts", reads: "range", appliesTo: TYPESCRIPT_SOURCE },
|
|
44
44
|
{ bin: "checks-feature-owners", script: "feature-owners.ts", reads: "range", appliesTo: TYPESCRIPT_SOURCE },
|
|
45
|
+
{ bin: "checks-quarantine-clock", script: "quarantine-clock.ts", reads: "range", appliesTo: EVERY_REPOSITORY },
|
|
45
46
|
] as const satisfies readonly KitGate[];
|
|
46
47
|
|
|
47
48
|
const UNCONDITIONAL = KIT_GATES.filter((gate) => gate.appliesTo === EVERY_REPOSITORY).map((gate) => gate.bin);
|
package/scripts/git.ts
CHANGED
|
@@ -148,6 +148,12 @@ function namesAParent(commitObject: string): boolean {
|
|
|
148
148
|
return headers.split("\n").some((header) => header.startsWith("parent "));
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
export const isShallowBoundary = Effect.fn("isShallowBoundary")(function* (rev: string, cwd?: string) {
|
|
152
|
+
if (!namesAParent(yield* git(["cat-file", "commit", rev], cwd))) return false;
|
|
153
|
+
const [, ...parents] = (yield* git(["rev-list", "--parents", "-n", "1", rev], cwd)).trim().split(" ");
|
|
154
|
+
return parents.length === 0;
|
|
155
|
+
});
|
|
156
|
+
|
|
151
157
|
// A shallow clone's boundary commit reads as parentless to rev-parse and log, and judged against
|
|
152
158
|
// the empty tree it would carry the whole repository; only the commit object still names its parents.
|
|
153
159
|
export const parentOrEmptyTree = Effect.fn("parentOrEmptyTree")(function* (rev: string, cwd?: string) {
|
|
@@ -172,6 +178,10 @@ export const rangeFromArgs = Effect.fn("rangeFromArgs")(function* (args: readonl
|
|
|
172
178
|
return yield* rangeEnds(first, second, cwd);
|
|
173
179
|
});
|
|
174
180
|
|
|
181
|
+
export const commitOf = Effect.fn("commitOf")(function* (rev: string, cwd?: string) {
|
|
182
|
+
return (yield* git(["rev-parse", "--verify", `${rev}^{commit}`], cwd)).trim();
|
|
183
|
+
});
|
|
184
|
+
|
|
175
185
|
// A scratch index leaves the repository's own index and working tree untouched.
|
|
176
186
|
export const checkoutFiles = Effect.fn("checkoutFiles")(function* (rev: string, files: readonly string[], scratch: string, cwd?: string) {
|
|
177
187
|
const path = yield* Path.Path;
|