@avi2dg/checks 0.55.0 → 0.56.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 +8 -0
- package/docs/bins.md +1 -0
- package/docs/configs/dependency-rules.md +1 -1
- package/docs/design.md +4 -3
- package/docs/gates/checks-ci-wiring.md +1 -1
- package/docs/gates/checks-copy.md +1 -1
- package/docs/gates/checks-css-size.md +1 -1
- package/docs/gates/checks-effect-scope.md +1 -1
- package/docs/gates/checks-frontend-syntax.md +1 -1
- package/docs/gates/checks-imports.md +1 -1
- package/docs/gates/checks-lint-coverage.md +1 -1
- package/docs/gates/checks-lint.md +12 -3
- package/docs/gates/checks-no-plain-js.md +1 -1
- package/docs/gates/checks-test-layout.md +1 -1
- package/docs/gates/checks-unused.md +1 -1
- package/package.json +3 -1
- package/src/core/lint.ts +40 -4
- package/src/core/scope.ts +54 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Every release of `@avi2dg/checks`, newest first, written by the release from its conventional commits.
|
|
4
4
|
|
|
5
|
+
## 0.56.0
|
|
6
|
+
|
|
7
|
+
Released 2026-10-27.
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- **core:** scope lint gates by changed paths and add desk run [#11](https://github.com/avi2d/checks/pull/11)
|
|
12
|
+
|
|
5
13
|
## 0.55.0
|
|
6
14
|
|
|
7
15
|
Released 2026-10-27.
|
package/docs/bins.md
CHANGED
|
@@ -10,6 +10,7 @@ The kit ships one bin per check, each named `checks-` and called from a package
|
|
|
10
10
|
|
|
11
11
|
`checks-lint` runs each of these gates that applies to the repository, and names every one that fails.
|
|
12
12
|
A gate reads either the working tree or the range `checks-lint` resolves.
|
|
13
|
+
It scopes tree gates to the change unless `--full` forces the full run, and `--desk` runs the fast tree gates.
|
|
13
14
|
The table groups the gates by vector, the part of a repository each one judges.
|
|
14
15
|
|
|
15
16
|
<!-- generated gates: bun run build writes it from KIT_GATES in src/core/gates.ts and scripts/doc-blocks.ts -->
|
|
@@ -61,7 +61,7 @@ A rule under `forbidden` that takes a kit rule's name replaces that rule whole,
|
|
|
61
61
|
|
|
62
62
|
## Running it
|
|
63
63
|
|
|
64
|
-
`checks-lint` runs [checks-imports](../gates/checks-imports.md), which cruises every tracked TypeScript file against `dependency-cruiser.config.ts`, or against the kit's defaults when the repository holds no config.
|
|
64
|
+
`checks-lint` runs [checks-imports](../gates/checks-imports.md) in the full run, which cruises every tracked TypeScript file against `dependency-cruiser.config.ts`, or against the kit's defaults when the repository holds no config.
|
|
65
65
|
|
|
66
66
|
## Related topics
|
|
67
67
|
|
package/docs/design.md
CHANGED
|
@@ -123,10 +123,11 @@ So the three packages move together at one exact version.
|
|
|
123
123
|
`checks-lint` runs each gate in a child process rather than importing it.
|
|
124
124
|
A gate then behaves the same alone or through `checks-lint`, and `lint-coverage.sh` can stay a shell script.
|
|
125
125
|
The gates run one at a time and pass their output straight through, so each report reads whole and in the order of the gate table.
|
|
126
|
-
`checks-lint` picks the gates that apply from the tracked files, and a repository cannot select gates.
|
|
127
|
-
A
|
|
126
|
+
`checks-lint` picks the gates that apply from the tracked files for the full run, and a repository cannot select gates.
|
|
127
|
+
A scoped run selects a subset by changed paths, and the desk run selects the fast tree gates.
|
|
128
|
+
A TypeScript gate applies as soon as the repository tracks TypeScript source, and `checks-lint-coverage` and `checks-unused` also apply on tracked Astro source.
|
|
128
129
|
|
|
129
|
-
dependency-cruiser runs as the `checks-imports` gate rather than as a script each repository adds to `lint`, so
|
|
130
|
+
dependency-cruiser runs as the `checks-imports` gate rather than as a script each repository adds to `lint`, so the full run keeps it.
|
|
130
131
|
A repository with no config of its own is cruised against the kit's defaults, since every rule has one.
|
|
131
132
|
The gate cruises the tracked TypeScript, so a build output or a linked library outside git never reaches the cruise.
|
|
132
133
|
It runs dependency-cruiser under Bun, which loads a TypeScript config wherever it sits, the kit's defaults under `node_modules` among them.
|
|
@@ -107,7 +107,7 @@ copy: no violation in 2 file(s) from 2 declared input(s)
|
|
|
107
107
|
|
|
108
108
|
## When it runs
|
|
109
109
|
|
|
110
|
-
`checks-lint` runs it when the repository tracks `copy.json` at its root.
|
|
110
|
+
`checks-lint` runs it in the full run when the repository tracks `copy.json` at its root.
|
|
111
111
|
A repository without that file never runs it and needs no html-validate.
|
|
112
112
|
|
|
113
113
|
## Related topics
|
|
@@ -55,7 +55,7 @@ effect-scope: the @effect/language-service entry in tsconfig.json differs from t
|
|
|
55
55
|
## When it runs
|
|
56
56
|
|
|
57
57
|
A repository runs `checks-effect-scope` from its `build` script, so CI's `git diff --exit-code` after the build fails on a stale `tsconfig.json` too.
|
|
58
|
-
`checks-lint` runs it with `--check` when the repository tracks `oxlint.config.ts`.
|
|
58
|
+
`checks-lint` runs it with `--check` in the full run when the repository tracks `oxlint.config.ts`.
|
|
59
59
|
|
|
60
60
|
## Related topics
|
|
61
61
|
|
|
@@ -177,7 +177,7 @@ frontend-syntax: no violation in 2 file(s) from 2 declared input(s)
|
|
|
177
177
|
|
|
178
178
|
## When it runs
|
|
179
179
|
|
|
180
|
-
`checks-lint` runs it when the repository tracks `frontend-syntax.json` at its root.
|
|
180
|
+
`checks-lint` runs it in the full run when the repository tracks `frontend-syntax.json` at its root.
|
|
181
181
|
A repository without that file never runs it and needs neither `stylelint.config.ts` nor the peers.
|
|
182
182
|
|
|
183
183
|
## Related topics
|
|
@@ -56,7 +56,7 @@ imports: 225 module(s) cruised against dependency-cruiser.config.ts, no violatio
|
|
|
56
56
|
|
|
57
57
|
## When it runs
|
|
58
58
|
|
|
59
|
-
`checks-lint` runs it when the repository tracks a `.ts` or `.tsx` file, so a repository with no config of its own still has its imports judged.
|
|
59
|
+
`checks-lint` runs it in the full run when the repository tracks a `.ts` or `.tsx` file, so a repository with no config of its own still has its imports judged, and never in the desk run.
|
|
60
60
|
|
|
61
61
|
## Related topics
|
|
62
62
|
|
|
@@ -53,7 +53,7 @@ lint-coverage: the program tsconfig.json builds holds the ts-reset rules is-arra
|
|
|
53
53
|
|
|
54
54
|
## When it runs
|
|
55
55
|
|
|
56
|
-
`checks-lint` runs it when the repository tracks a `.ts`, `.tsx` or `.astro` file.
|
|
56
|
+
`checks-lint` runs it in the full run when the repository tracks a `.ts`, `.tsx` or `.astro` file.
|
|
57
57
|
|
|
58
58
|
## Related topics
|
|
59
59
|
|
|
@@ -4,11 +4,16 @@ audience: consumers
|
|
|
4
4
|
---
|
|
5
5
|
# checks-lint
|
|
6
6
|
|
|
7
|
-
`checks-lint` runs every applicable kit lint gate over one range and reports each failure.
|
|
7
|
+
`checks-lint` runs every applicable kit lint gate in the selected set over one range and reports each failure.
|
|
8
8
|
|
|
9
9
|
## What it checks
|
|
10
10
|
|
|
11
11
|
It runs the gates under [What runs](../bins.md#what-runs), each in its own process.
|
|
12
|
+
It scopes the run to the gates the change reaches, and the full run stays the fallback.
|
|
13
|
+
Applicability below selects the full run, and a scoped or desk run selects a subset.
|
|
14
|
+
A Markdown change runs the range gates plus `checks-copy`.
|
|
15
|
+
A TypeScript source change runs every applicable gate.
|
|
16
|
+
A test file classifies as TypeScript, so its change runs every applicable gate too.
|
|
12
17
|
Gates requiring tracked TypeScript files begin running when the repository tracks TypeScript.
|
|
13
18
|
`checks-lint-coverage` and `checks-unused` also begin running when the repository tracks an `.astro` file.
|
|
14
19
|
`checks-advisories` begins running when the repository tracks `bun.lock`.
|
|
@@ -31,16 +36,19 @@ Tree gates read the working tree rather than the range.
|
|
|
31
36
|
|
|
32
37
|
```sh
|
|
33
38
|
checks-lint
|
|
34
|
-
checks-lint
|
|
39
|
+
checks-lint [--full | --desk]
|
|
40
|
+
checks-lint [--full | --desk] <base-ref> <head-ref>
|
|
35
41
|
```
|
|
36
42
|
|
|
37
43
|
With two arguments, the base and head override range discovery.
|
|
44
|
+
`--full` forces the unscoped run.
|
|
45
|
+
`--desk` runs the fast tree gates.
|
|
38
46
|
|
|
39
47
|
## Exit codes
|
|
40
48
|
|
|
41
49
|
| Code | Result |
|
|
42
50
|
| --- | --- |
|
|
43
|
-
| 0 | Every
|
|
51
|
+
| 0 | Every gate that ran passed. |
|
|
44
52
|
| 1 | At least one gate found a violation. |
|
|
45
53
|
| 2 | The range could not resolve or no failed gate could decide. |
|
|
46
54
|
|
|
@@ -60,6 +68,7 @@ checks-lint: 1 of 19 gate(s) failed: checks-comment-gate
|
|
|
60
68
|
A repository runs it from `bun run lint` in a pull request workflow that fetches the whole git history.
|
|
61
69
|
It leaves out the TypeScript gates while the repository tracks no TypeScript file, except that `checks-lint-coverage` and `checks-unused` run when it tracks an `.astro` file.
|
|
62
70
|
It never runs `checks-browser`, which a product runs after it builds its site.
|
|
71
|
+
A pull request run scopes the gates to the change.
|
|
63
72
|
|
|
64
73
|
## Related topics
|
|
65
74
|
|
|
@@ -87,7 +87,7 @@ test-layout: 4 violation(s)
|
|
|
87
87
|
|
|
88
88
|
## When it runs
|
|
89
89
|
|
|
90
|
-
`checks-lint` runs it when the repository tracks TypeScript.
|
|
90
|
+
`checks-lint` runs it in the full run when the repository tracks TypeScript.
|
|
91
91
|
A repository without TypeScript needs neither the `checks-test` script nor `bunfig.toml`.
|
|
92
92
|
|
|
93
93
|
## Related topics
|
|
@@ -57,7 +57,7 @@ unused: no unreferenced files among 110 tracked .ts/.tsx/.astro file(s)
|
|
|
57
57
|
|
|
58
58
|
## When it runs
|
|
59
59
|
|
|
60
|
-
`checks-lint` runs it when the repository tracks a `.ts`, `.tsx` or `.astro` file.
|
|
60
|
+
`checks-lint` runs it in the full run when the repository tracks a `.ts`, `.tsx` or `.astro` file, and never in the desk run.
|
|
61
61
|
Such a repository names its entries in a Knip configuration.
|
|
62
62
|
|
|
63
63
|
## Related topics
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avi2dg/checks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.56.0",
|
|
4
4
|
"description": "Deterministic checks shared across a set of TypeScript repositories",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "avi2d",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"src/core/gates.ts",
|
|
35
35
|
"src/core/declared-inputs.ts",
|
|
36
36
|
"src/core/lint.ts",
|
|
37
|
+
"src/core/scope.ts",
|
|
37
38
|
"src/delivery/changelog.ts",
|
|
38
39
|
"src/delivery/changelog-write.ts",
|
|
39
40
|
"src/delivery/release-notes.ts",
|
|
@@ -205,6 +206,7 @@
|
|
|
205
206
|
},
|
|
206
207
|
"scripts": {
|
|
207
208
|
"prepare": "bun src/dependencies/vendor.ts --library effect --package effect --repository https://github.com/Effect-TS/effect.git --tag 'effect@{version}' --path packages/effect/package.json",
|
|
209
|
+
"desk": "bun run test:fast && bun src/core/lint.ts --desk",
|
|
208
210
|
"build": "bun build src/quality/effect-channel/index.ts --outdir dist/effect-channel --target node --format esm && bun build src/complexity/readability/index.ts --outdir dist/readability --target node --format esm && bun build src/quality/data-shape/index.ts --outdir dist/data-shape --target node --format esm && bun build src/testing/mutation-guard-plugin.ts src/testing/mutation-refusal.ts src/testing/unit-test-order.ts --outdir dist/mutation --target node --format esm --packages external && bun build src/quality/presets/oxlint.ts src/quality/presets/knip.ts src/quality/presets/dependency-cruiser.ts src/quality/presets/stylelint.ts --outdir dist/presets --target node --format esm --packages external && tsc -p tsconfig.presets.json && bun scripts/configs-write.ts && bun src/quality/effect-scope.ts && bun scripts/doc-templates-write.ts && bun src/delivery/changelog-write.ts && bun scripts/doc-blocks-write.ts",
|
|
209
211
|
"lint": "oxlint && bun src/core/lint.ts",
|
|
210
212
|
"typecheck": "tsc --noEmit && effect-tsgo diagnostics --project tsconfig.json --format text --strict",
|
package/src/core/lint.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
import { Config, Console, Effect, FileSystem, Option, Path, Schema } from "effect";
|
|
3
3
|
import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
4
4
|
import { EVERY_REPOSITORY, KIT_GATES, type KitGate } from "./gates.ts";
|
|
5
|
-
import { defaultBranch, git } from "./git.ts";
|
|
5
|
+
import { changedPaths, defaultBranch, git } from "./git.ts";
|
|
6
6
|
import { runMain, Usage } from "./main.ts";
|
|
7
|
+
import { FAST_TREE_GATES, selectScope } from "./scope.ts";
|
|
7
8
|
|
|
8
9
|
type Range = {
|
|
9
10
|
readonly refs: readonly [tip: string] | readonly [base: string, head: string];
|
|
@@ -21,7 +22,7 @@ class GatesUndecided extends Schema.TaggedError<GatesUndecided>()("GatesUndecide
|
|
|
21
22
|
}) {}
|
|
22
23
|
|
|
23
24
|
const NAME = "checks-lint";
|
|
24
|
-
const USAGE = "usage: lint.ts [<base-ref> <head-ref>]";
|
|
25
|
+
const USAGE = "usage: lint.ts [--full | --desk] [<base-ref> <head-ref>]";
|
|
25
26
|
const PULL_REQUEST_EVENT = "pull_request";
|
|
26
27
|
const SHALLOW_HINT = "a CI checkout needs actions/checkout fetch-depth: 0";
|
|
27
28
|
|
|
@@ -140,6 +141,15 @@ const resolveRange = Effect.fn("resolveRange")(function* (args: readonly string[
|
|
|
140
141
|
return { refs: rangeOf(base, head), source: ends.source } satisfies Range;
|
|
141
142
|
});
|
|
142
143
|
|
|
144
|
+
export type LintMode = "full" | "desk" | "scoped";
|
|
145
|
+
|
|
146
|
+
export function splitMode(args: readonly string[]): { readonly mode: LintMode; readonly rest: readonly string[] } | undefined {
|
|
147
|
+
const flags = args.filter((arg) => arg.startsWith("--"));
|
|
148
|
+
const rest = args.filter((arg) => !arg.startsWith("--"));
|
|
149
|
+
if (flags.length > 1 || (flags.length === 1 && flags[0] !== "--full" && flags[0] !== "--desk")) return undefined;
|
|
150
|
+
return { mode: flags.length === 0 ? "scoped" : flags[0] === "--full" ? "full" : "desk", rest };
|
|
151
|
+
}
|
|
152
|
+
|
|
143
153
|
export function describe({ refs }: Range): string {
|
|
144
154
|
return refs.length === 1 ? `tip ${refs[0]}` : `range ${refs[0]}..${refs[1]}`;
|
|
145
155
|
}
|
|
@@ -165,15 +175,41 @@ const runGate = Effect.fn("runGate")(function* (gate: KitGate, range: Range) {
|
|
|
165
175
|
return { gate, outcome: outcomeOf(exitCode) };
|
|
166
176
|
});
|
|
167
177
|
|
|
178
|
+
const scopedGates = Effect.fn("scopedGates")(function* (range: Range, root: string) {
|
|
179
|
+
if (range.refs.length === 1) return { bins: undefined, detail: "the full run, since a lone tip scopes nothing" };
|
|
180
|
+
const [base, head] = range.refs;
|
|
181
|
+
const changes = yield* changedPaths(base, head, [], root);
|
|
182
|
+
const committed = changes.flatMap((change) => (change.kind === "renamed" ? [change.from, change.path] : [change.path]));
|
|
183
|
+
const working = (yield* git(["diff", "--no-renames", "--name-only", "-z", "HEAD", "--"], root)).split("\0").filter(Boolean);
|
|
184
|
+
const untracked = (yield* git(["ls-files", "--others", "--exclude-standard", "-z"], root)).split("\0").filter(Boolean);
|
|
185
|
+
const scope = selectScope([...committed, ...working, ...untracked]);
|
|
186
|
+
if (scope.full) return { bins: undefined, detail: "the full run, since the change reaches past a scoped shape" };
|
|
187
|
+
return { bins: scope.gates, detail: "" };
|
|
188
|
+
});
|
|
189
|
+
|
|
168
190
|
const lint = Effect.gen(function* () {
|
|
191
|
+
const split = splitMode(process.argv.slice(2));
|
|
192
|
+
if (split === undefined) return yield* new Usage({ message: USAGE });
|
|
169
193
|
const root = (yield* git(["rev-parse", "--show-toplevel"])).trim();
|
|
170
|
-
const range = yield* resolveRange(
|
|
194
|
+
const range = yield* resolveRange(split.rest);
|
|
171
195
|
yield* Console.log(`${NAME}: ${describe(range)} from ${range.source}`);
|
|
172
|
-
const
|
|
196
|
+
const applicable = yield* Effect.filter(KIT_GATES, (gate) =>
|
|
173
197
|
gate.appliesTo === EVERY_REPOSITORY
|
|
174
198
|
? Effect.succeed(true)
|
|
175
199
|
: git(["ls-files", "--", ...gate.appliesTo.pathspecs], root).pipe(Effect.map((files) => files.trim() !== "")),
|
|
176
200
|
);
|
|
201
|
+
const scoped = split.mode === "desk"
|
|
202
|
+
? {
|
|
203
|
+
bins: applicable.filter((gate) => FAST_TREE_GATES.includes(gate.bin)).map((gate) => gate.bin),
|
|
204
|
+
detail: "the desk run",
|
|
205
|
+
}
|
|
206
|
+
: split.mode === "full"
|
|
207
|
+
? { bins: applicable.map((gate) => gate.bin), detail: "the full run (--full)" }
|
|
208
|
+
: yield* scopedGates(range, root);
|
|
209
|
+
const bins = scoped.bins;
|
|
210
|
+
const gates = bins === undefined ? applicable : applicable.filter((gate) => bins.includes(gate.bin));
|
|
211
|
+
const detail = split.mode === "scoped" && bins !== undefined ? `${gates.length} gate(s) for this change` : scoped.detail;
|
|
212
|
+
yield* Console.log(`${NAME}: ${detail}`);
|
|
177
213
|
const verdicts = yield* Effect.forEach(gates, (gate) => runGate(gate, range));
|
|
178
214
|
const failed = verdicts.filter((verdict) => verdict.outcome !== "passed");
|
|
179
215
|
if (failed.length === 0) {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { KIT_GATES } from "./gates.ts";
|
|
2
|
+
|
|
3
|
+
export type Scope = {
|
|
4
|
+
readonly full: boolean;
|
|
5
|
+
readonly gates: readonly string[];
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type ChangeShape = "docs" | "copy" | "typescript" | "workflow" | "lockfile" | "unknown";
|
|
9
|
+
|
|
10
|
+
const TREE_GATES = KIT_GATES.filter((gate) => gate.reads === "tree").map((gate) => gate.bin);
|
|
11
|
+
|
|
12
|
+
// checks-unused and checks-imports each take over a second, so the desk run skips them.
|
|
13
|
+
const SLOW_TREE_GATES: readonly string[] = ["checks-unused", "checks-imports"];
|
|
14
|
+
|
|
15
|
+
export const FAST_TREE_GATES: readonly string[] = TREE_GATES.filter((bin) => !SLOW_TREE_GATES.includes(bin));
|
|
16
|
+
|
|
17
|
+
const CI_CONFIGS = new Set([
|
|
18
|
+
"package.json",
|
|
19
|
+
"bunfig.toml",
|
|
20
|
+
"commitlint.config.ts",
|
|
21
|
+
"oxlint.config.ts",
|
|
22
|
+
"knip.config.ts",
|
|
23
|
+
"knip-base.json",
|
|
24
|
+
"dependency-cruiser.config.js",
|
|
25
|
+
"dependency-cruiser.config.ts",
|
|
26
|
+
]);
|
|
27
|
+
const basenameOf = (path: string): string => path.slice(path.lastIndexOf("/") + 1);
|
|
28
|
+
|
|
29
|
+
export function classifyPath(path: string): ChangeShape {
|
|
30
|
+
const basename = basenameOf(path);
|
|
31
|
+
if (basename === "bun.lock") return "lockfile";
|
|
32
|
+
if (path.startsWith(".github/") || CI_CONFIGS.has(basename) || /^tsconfig(\..*)?\.json$/.test(basename)) {
|
|
33
|
+
return "workflow";
|
|
34
|
+
}
|
|
35
|
+
if (path === "copy.json") return "copy";
|
|
36
|
+
if (/\.(?:ts|tsx|mts|cts|astro)$/.test(path)) return "typescript";
|
|
37
|
+
if (path.endsWith(".md")) return "docs";
|
|
38
|
+
return "unknown";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const fullScope = (): Scope => ({ full: true, gates: KIT_GATES.map((gate) => gate.bin) });
|
|
42
|
+
|
|
43
|
+
export function selectScope(paths: readonly string[]): Scope {
|
|
44
|
+
if (paths.length === 0) return fullScope();
|
|
45
|
+
const shapes = new Set(paths.map(classifyPath));
|
|
46
|
+
if (shapes.has("unknown") || shapes.has("workflow") || shapes.has("lockfile")) return fullScope();
|
|
47
|
+
const copyContent = shapes.has("copy") || shapes.has("docs");
|
|
48
|
+
const tree = shapes.has("typescript") ? TREE_GATES : copyContent ? ["checks-copy"] : [];
|
|
49
|
+
return {
|
|
50
|
+
full: false,
|
|
51
|
+
gates: KIT_GATES.filter((gate) => gate.reads === "range" || tree.includes(gate.bin)).map((gate) => gate.bin),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|