@avi2dg/checks 0.12.0 → 0.14.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 +124 -0
- package/CONTRIBUTING.md +89 -0
- package/README.md +137 -1230
- package/dist/feature-rules.js +14 -1
- package/docs/configs/commit-messages.md +43 -0
- package/docs/configs/dependency-rules.md +62 -0
- package/docs/configs/effect-rules.md +48 -0
- package/docs/configs/quality-file.md +99 -0
- package/docs/design.md +77 -0
- package/docs/gates/checks-backtest.md +50 -0
- package/docs/gates/checks-ci-wiring.md +122 -0
- package/docs/gates/checks-comment-gate.md +58 -0
- package/docs/gates/checks-commit-identity.md +63 -0
- package/docs/gates/checks-docs.md +98 -0
- package/docs/gates/checks-feature-owners.md +113 -0
- package/docs/gates/checks-flake.md +88 -0
- package/docs/gates/checks-lint-coverage.md +49 -0
- package/docs/gates/checks-lint.md +136 -0
- package/docs/gates/checks-mutation-compare.md +84 -0
- package/docs/gates/checks-quality.md +94 -0
- package/docs/gates/checks-size-budget.md +64 -0
- package/docs/gates/checks-suppressions-ratchet.md +51 -0
- package/docs/gates/checks-test-layout.md +77 -0
- package/docs/gates/checks-test.md +75 -0
- package/package.json +24 -3
- package/quality.schema.json +48 -0
- package/scripts/doc-outline.ts +215 -0
- package/scripts/doc-rules.ts +177 -0
- package/scripts/doc-templates.ts +208 -0
- package/scripts/docs.ts +90 -0
- package/scripts/gates.ts +1 -0
- package/scripts/quality-file.ts +22 -1
- package/templates/adr.md +29 -0
- package/templates/agents.md +17 -0
- package/templates/changelog.md +37 -0
- package/templates/claude.md +2 -0
- package/templates/explanation.md +15 -0
- package/templates/how-to.md +33 -0
- package/templates/readme.md +45 -0
- package/templates/reference.md +15 -0
- package/templates/tutorial.md +31 -0
package/dist/feature-rules.js
CHANGED
|
@@ -17,6 +17,7 @@ var KIT_GATES = [
|
|
|
17
17
|
{ bin: "checks-comment-gate", script: "comment-gate.ts", reads: "range", appliesTo: EVERY_REPOSITORY },
|
|
18
18
|
{ bin: "checks-suppressions-ratchet", script: "suppressions-ratchet.ts", reads: "range", appliesTo: EVERY_REPOSITORY },
|
|
19
19
|
{ bin: "checks-ci-wiring", script: "ci-wiring.ts", reads: "tree", appliesTo: EVERY_REPOSITORY },
|
|
20
|
+
{ bin: "checks-docs", script: "docs.ts", reads: "range", appliesTo: EVERY_REPOSITORY },
|
|
20
21
|
{ bin: "checks-quality", script: "quality.ts", reads: "tree", args: ["--check"], appliesTo: QUALITY_DECLARATION },
|
|
21
22
|
{ bin: "checks-size-budget", script: "size-budget.ts", reads: "range", appliesTo: TYPESCRIPT_SOURCE },
|
|
22
23
|
{ bin: "checks-feature-owners", script: "feature-owners.ts", reads: "range", appliesTo: TYPESCRIPT_SOURCE }
|
|
@@ -123,6 +124,17 @@ var AgentRules = Schema2.Struct({
|
|
|
123
124
|
const both = on.filter((rule) => off.includes(rule));
|
|
124
125
|
return both.length === 0 || `switches ${both.join(", ")} both on and off`;
|
|
125
126
|
}));
|
|
127
|
+
var pagesIn = (mode) => Schema2.optionalKey(Schema2.Array(PathGlob).annotate({ description: `The pages written as ${mode}` }));
|
|
128
|
+
var Docs = Schema2.Struct({
|
|
129
|
+
pages: Schema2.optionalKey(Schema2.Struct({
|
|
130
|
+
tutorial: pagesIn("a tutorial, which teaches by building one thing"),
|
|
131
|
+
"how-to": pagesIn("a how-to, which walks one task"),
|
|
132
|
+
reference: pagesIn("reference, which describes a thing to be looked up"),
|
|
133
|
+
explanation: pagesIn("an explanation, which says why")
|
|
134
|
+
}).annotate({
|
|
135
|
+
description: "The Diátaxis mode of each page, whose template checks-docs holds the page to; a page under docs/ needs one"
|
|
136
|
+
}))
|
|
137
|
+
});
|
|
126
138
|
var Quality = Schema2.Struct({
|
|
127
139
|
$schema: Schema2.optionalKey(Schema2.String),
|
|
128
140
|
defaultBranch: Schema2.optionalKey(Schema2.NonEmptyString.annotate({ description: "The branch pull requests merge into; main when absent" })),
|
|
@@ -136,7 +148,8 @@ var Quality = Schema2.Struct({
|
|
|
136
148
|
changeSignal: Schema2.optionalKey(Schema2.Literal("advisory").annotate({
|
|
137
149
|
description: "Report which feature owners a change touches, without failing on it"
|
|
138
150
|
})),
|
|
139
|
-
agentRules: Schema2.optionalKey(AgentRules)
|
|
151
|
+
agentRules: Schema2.optionalKey(AgentRules),
|
|
152
|
+
docs: Schema2.optionalKey(Docs.annotate({ description: "What checks-docs reads to map a doc file to its template" }))
|
|
140
153
|
}).annotate({
|
|
141
154
|
title: QUALITY_FILE,
|
|
142
155
|
description: "What a repository has opted into from @avi2dg/checks, read by its bins and agent Rule selection"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# The commit message lint
|
|
2
|
+
|
|
3
|
+
The shared commitlint config holds each pull request title to conventional commits, and a reader looks it up to wire the lint into a repository's CI.
|
|
4
|
+
|
|
5
|
+
## Config
|
|
6
|
+
|
|
7
|
+
Commits follow `@commitlint/config-conventional` plus the house prefixes `commitlint.config.js` lists, shared from `@avi2dg/checks/commitlint.config.js`.
|
|
8
|
+
It arrives with the kit, since `@commitlint/cli` and `@commitlint/config-conventional` are dependencies, not peers.
|
|
9
|
+
|
|
10
|
+
## Workflow
|
|
11
|
+
|
|
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
|
+
```
|
|
31
|
+
|
|
32
|
+
## What it lints
|
|
33
|
+
|
|
34
|
+
It lints the pull request title and nothing else.
|
|
35
|
+
The title is the enforced subject because a squash merge uses it as the main commit subject, and per-commit messages are not linted.
|
|
36
|
+
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.
|
|
37
|
+
|
|
38
|
+
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.
|
|
39
|
+
[checks-commit-identity](../gates/checks-commit-identity.md) is that enforcement.
|
|
40
|
+
|
|
41
|
+
## Related topics
|
|
42
|
+
|
|
43
|
+
- [checks-commit-identity](../gates/checks-commit-identity.md)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# The dependency rules
|
|
2
|
+
|
|
3
|
+
The shared dependency-cruiser base holds a repository's imports to a set of rules every repository shares, and a reader looks it up to add a boundary of its own.
|
|
4
|
+
|
|
5
|
+
## Base rules
|
|
6
|
+
|
|
7
|
+
`.dependency-cruiser.cjs` extends the shared base, which carries these rules:
|
|
8
|
+
|
|
9
|
+
- `no-circular`
|
|
10
|
+
- `no-orphans`
|
|
11
|
+
- `not-to-dev-dep`, which refuses shipped source importing a dev-only package, and a package listed in `peerDependencies` too is not dev-only
|
|
12
|
+
- `not-to-unresolvable`, which refuses a specifier nothing installed answers
|
|
13
|
+
- `no-deep-imports`, which refuses a subpath the package's exports map does not publish
|
|
14
|
+
|
|
15
|
+
The base parses with swc, so it needs `@swc/core` installed, and without it the cruise silently skips every `.ts` file.
|
|
16
|
+
|
|
17
|
+
## Boundaries
|
|
18
|
+
|
|
19
|
+
A repository appends a named rule per boundary it owns:
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
module.exports = {
|
|
23
|
+
extends: "./node_modules/@avi2dg/checks/dependency-cruiser.config.js",
|
|
24
|
+
forbidden: [
|
|
25
|
+
{
|
|
26
|
+
name: "ui-cannot-reach-server",
|
|
27
|
+
severity: "error",
|
|
28
|
+
from: { path: "^src/ui" },
|
|
29
|
+
to: { path: "^src/server" },
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
A rule that restates a base name overrides it field by field.
|
|
36
|
+
That is how an entry point stops being an orphan: redeclare `no-orphans` with the entry added to its `pathNot`.
|
|
37
|
+
A repository that declares feature owners spreads the rules `quality.json` compiles to into the same `forbidden`, as [checks-feature-owners](../gates/checks-feature-owners.md#import-boundary) says.
|
|
38
|
+
|
|
39
|
+
## Running it
|
|
40
|
+
|
|
41
|
+
`package.json` gains the script:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
"lint:deps": "depcruise --config .dependency-cruiser.cjs src"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
CI runs it beside the other checks:
|
|
48
|
+
|
|
49
|
+
```yaml
|
|
50
|
+
jobs:
|
|
51
|
+
lint:
|
|
52
|
+
steps:
|
|
53
|
+
- uses: actions/checkout@v5
|
|
54
|
+
- uses: oven-sh/setup-bun@v2
|
|
55
|
+
- run: bun install --frozen-lockfile
|
|
56
|
+
- run: bun run lint:deps
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Related topics
|
|
60
|
+
|
|
61
|
+
- [checks-feature-owners](../gates/checks-feature-owners.md)
|
|
62
|
+
- [Why it is shaped this way](../design.md)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# The Effect rules
|
|
2
|
+
|
|
3
|
+
The oxlint base config and the tsconfig fragment hold a repository's Effect code to its error channel and to Effect-native IO, and a reader looks them up to learn what each rule refuses and how a path opts in.
|
|
4
|
+
|
|
5
|
+
## Base config
|
|
6
|
+
|
|
7
|
+
The base config, `oxlintrc.json`, loads the `effect-channel` plugin and turns on `effect-channel/no-error-channel-escape`.
|
|
8
|
+
That rule refuses `Effect.ignore`, `Effect.ignoreCause`, the `Effect.catchCause` family, and an `Effect.catch` whose handler takes no error or names it `_`.
|
|
9
|
+
|
|
10
|
+
Two more rules ship off, because a repository writes only some of its paths in Effect, and code a host loads without `node_modules`, such as a hook bundle or this oxlint plugin, cannot import it:
|
|
11
|
+
|
|
12
|
+
- `effect-channel/no-throw` refuses a `throw` statement.
|
|
13
|
+
- `effect-channel/no-try-catch` refuses a `try` statement with a `catch` clause, and `try`/`finally` stays allowed.
|
|
14
|
+
|
|
15
|
+
Each refusal says what to write instead: a `Schema.TaggedError` failed through `Effect.fail`, a throwing call wrapped in `Effect.try` or `Effect.tryPromise`, and recovery by tag with `Effect.catchTag`.
|
|
16
|
+
|
|
17
|
+
## Effect paths
|
|
18
|
+
|
|
19
|
+
A repository turns the rules on for the paths it writes in Effect by declaring those paths in `quality.json` and extending the fragments [checks-quality](../gates/checks-quality.md) generates:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
"sources": {
|
|
23
|
+
"effect": { "paths": ["src/**/*.ts"], "exempt": ["src/host/*.ts"] }
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The fragment's override carries the kit's Effect rule block, `presets/effect.oxlint.json`.
|
|
28
|
+
It holds the two rules above, plus `node/no-sync`, `oxc/no-async-await`, `promise/avoid-new` and `unicorn/no-process-exit`.
|
|
29
|
+
Files under `exempt` answer to none of them.
|
|
30
|
+
|
|
31
|
+
oxlint resolves `files` against the directory of the config that holds the override, so a config passed with `-c` from outside the repository matches nothing and reports nothing.
|
|
32
|
+
|
|
33
|
+
`unicorn/no-process-exit` passes over any file that opens with a shebang.
|
|
34
|
+
A repository whose bins open with one bans `process.exit` itself with `no-restricted-properties` in its own `.oxlintrc.json`, as the kit's own repository does.
|
|
35
|
+
The preset leaves that rule out because a repository's own `no-restricted-properties` list for the same files would replace it, or be replaced by it.
|
|
36
|
+
Sites standing when the declaration lands go in oxlint's own baseline, `oxlint --suppress-all`, so their count can only fall, as [checks-suppressions-ratchet](../gates/checks-suppressions-ratchet.md) holds.
|
|
37
|
+
|
|
38
|
+
## Language service
|
|
39
|
+
|
|
40
|
+
The language service holds the same paths to Effect-native IO through the tsconfig fragment's override, whose severities are `presets/effect.language-service.json`.
|
|
41
|
+
`nodeBuiltinImport`, `asyncFunction`, `newPromise` and `extendsNativeError` are all errors.
|
|
42
|
+
effect-tsgo keeps the severities `tsconfig.effect.json` sets when a later config in `extends` restates the plugin with only its overrides.
|
|
43
|
+
|
|
44
|
+
## Related topics
|
|
45
|
+
|
|
46
|
+
- [checks-quality](../gates/checks-quality.md)
|
|
47
|
+
- [The quality file](quality-file.md)
|
|
48
|
+
- [Why it is shaped this way](../design.md)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# The quality file
|
|
2
|
+
|
|
3
|
+
`quality.json` at the repository root says what the repository has opted into, and a reader looks it up to learn what each key holds and which bin reads it.
|
|
4
|
+
|
|
5
|
+
## Keys
|
|
6
|
+
|
|
7
|
+
The kit's bins find the file at the git root and read it there:
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"$schema": "./node_modules/@avi2dg/checks/quality.schema.json",
|
|
12
|
+
"defaultBranch": "main",
|
|
13
|
+
"gates": {
|
|
14
|
+
"ci": ["bun run lint", "bun run typecheck", "bun run test"],
|
|
15
|
+
"scheduled": ["bunx checks-flake --runs 10 --report flake-report.json"]
|
|
16
|
+
},
|
|
17
|
+
"commitIdentity": { "authors": [{ "name": "avi2d", "email": "avi2dg@gmail.com" }] },
|
|
18
|
+
"sources": {
|
|
19
|
+
"production": ["src/**/*.ts"],
|
|
20
|
+
"effect": { "paths": ["src/**/*.ts"], "exempt": ["src/host/*.ts"] }
|
|
21
|
+
},
|
|
22
|
+
"size": { "fileLines": 400, "functionLines": 100, "applies": "changed" },
|
|
23
|
+
"features": [
|
|
24
|
+
{
|
|
25
|
+
"name": "billing",
|
|
26
|
+
"root": "src/billing",
|
|
27
|
+
"entries": ["src/billing/index.ts"],
|
|
28
|
+
"allowFrom": ["src/main.ts"],
|
|
29
|
+
"proof": "tests/e2e/billing.test.ts"
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"changeSignal": "advisory",
|
|
33
|
+
"agentRules": { "on": [], "off": [] },
|
|
34
|
+
"docs": { "pages": { "reference": ["docs/gates/*.md"], "explanation": ["docs/design.md"] } }
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
<!-- generated quality-keys: bun run build writes it from Quality in scripts/quality-file.ts and scripts/doc-blocks.ts -->
|
|
39
|
+
|
|
40
|
+
| Key | Read by | Holds |
|
|
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 |
|
|
44
|
+
| `gates.scheduled` | `checks-ci-wiring` | the commands a schedule runs |
|
|
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
|
+
| `commitIdentity.authors` | `checks-commit-identity` | the identities allowed to author and commit, as [checks-commit-identity](../gates/checks-commit-identity.md) says |
|
|
47
|
+
| `sources.production` | `checks-size-budget`, `checks-quality` | the source the repository ships, as [checks-size-budget](../gates/checks-size-budget.md) says |
|
|
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
|
+
| `size` | `checks-size-budget` | the line budget, and which production files it holds |
|
|
50
|
+
| `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
|
+
| `changeSignal` | `checks-feature-owners` | `advisory` to list the feature owners a change touches |
|
|
52
|
+
| `agentRules.on` | agent Rule selection, not the kit | catalogued Rules switched on for this repository |
|
|
53
|
+
| `agentRules.off` | agent Rule selection, not the kit | catalogued Rules switched off for this repository |
|
|
54
|
+
| `docs.pages` | `checks-docs` | the Diátaxis mode of each page, by glob, as [checks-docs](../gates/checks-docs.md) says |
|
|
55
|
+
|
|
56
|
+
<!-- end generated quality-keys -->
|
|
57
|
+
|
|
58
|
+
Every key is optional.
|
|
59
|
+
|
|
60
|
+
## Schema
|
|
61
|
+
|
|
62
|
+
The bins decode the file with one Effect `Schema`, and the package ships `quality.schema.json` emitted from that schema.
|
|
63
|
+
The `$schema` line therefore gives an editor the verdict the bins reach, save what no JSON Schema can express across two values, which the bins refuse:
|
|
64
|
+
|
|
65
|
+
- a Rule switched both on and off
|
|
66
|
+
- a feature entry outside its root
|
|
67
|
+
- two features with one name or sharing a root
|
|
68
|
+
|
|
69
|
+
A key the schema does not name is refused, not ignored, so a misspelt `sources` cannot switch the Effect rules off unnoticed.
|
|
70
|
+
|
|
71
|
+
## Globs
|
|
72
|
+
|
|
73
|
+
A glob in `sources` starts at the repository root, names a directory first, uses `*` only within a segment and `**` only as a whole one, and ends in a file name with an extension.
|
|
74
|
+
Those are the globs oxlint, the language service and git all read alike.
|
|
75
|
+
oxlint matches `*.ts` at any depth where the other two match it at the root alone, so the schema refuses it, and `**/*.ts` means every depth to all three.
|
|
76
|
+
The language service matches nothing for `src/**` and oxlint nothing for `src/lib`, so the schema refuses both, and `src/**/*.ts` and `src/lib/*.ts` say it to all three.
|
|
77
|
+
|
|
78
|
+
## Keys moved from package.json
|
|
79
|
+
|
|
80
|
+
A repository with no `quality.json` still has `ciWiring` and `commitIdentity` read from `package.json`, with a notice on each read.
|
|
81
|
+
One with both files exits 2 until `package.json` drops them.
|
|
82
|
+
The keys map one for one:
|
|
83
|
+
|
|
84
|
+
<!-- generated legacy-keys: bun run build writes it from LegacyManifest in scripts/quality-file.ts, QUALITY_FILE in scripts/gates.ts and scripts/doc-blocks.ts -->
|
|
85
|
+
|
|
86
|
+
| `package.json` | `quality.json` |
|
|
87
|
+
| --- | --- |
|
|
88
|
+
| `ciWiring.gates` | `gates.ci` |
|
|
89
|
+
| `ciWiring.scheduled` | `gates.scheduled` |
|
|
90
|
+
| `ciWiring.lintGates` | `gates.lint` |
|
|
91
|
+
| `ciWiring.defaultBranch` | `defaultBranch` |
|
|
92
|
+
| `commitIdentity` | `commitIdentity` |
|
|
93
|
+
|
|
94
|
+
<!-- end generated legacy-keys -->
|
|
95
|
+
|
|
96
|
+
## Related topics
|
|
97
|
+
|
|
98
|
+
- [checks-quality](../gates/checks-quality.md)
|
|
99
|
+
- [Why it is shaped this way](../design.md)
|
package/docs/design.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Why it is shaped this way
|
|
2
|
+
|
|
3
|
+
Each entry below is a choice in the kit's shape and the constraint that forced it.
|
|
4
|
+
|
|
5
|
+
- Every config in an oxlint `extends` chain brings its own `plugins`, and one that sets none brings oxlint's default plugins, whose category rules the base's `categories` then turn on across the tree.
|
|
6
|
+
`rules`, `categories` and `jsPlugins` inherit as expected.
|
|
7
|
+
That is why the consumer snippet restates `plugins` and nothing else, and why the generated fragment always sets them.
|
|
8
|
+
- `node_modules/` is excluded through the consumer's `.gitignore`, not `ignorePatterns`: oxlint still walks the installed package when only `ignorePatterns` names it.
|
|
9
|
+
- `files` in package.json is the published surface: `tests/`, `AGENTS.md` and the `.ts` plugin source never reach an install.
|
|
10
|
+
npm adds `package.json`, `README` and `LICENSE` to the tarball whatever `files` says.
|
|
11
|
+
`bun pm pack` builds the same tarball the registry serves, which is what the packed-tarball consumer e2e test installs.
|
|
12
|
+
- The plugin ships compiled as `dist/index.js`, built with `bun build effect-channel/index.ts --outdir dist --target node --format esm`.
|
|
13
|
+
Node refuses to type-strip a `.ts` plugin under `node_modules`, so the `.ts` source would fail to load from an installed package.
|
|
14
|
+
- `featureRules` ships compiled as `dist/feature-rules.js` for the same reason, with `effect` left out of the bundle so it resolves the consumer's own copy.
|
|
15
|
+
dependency-cruiser uses a config's export as it is and never awaits it, so the declaration decodes synchronously, and `quality.json` exempts that one file from the Effect rules.
|
|
16
|
+
- `checks-size-budget` writes the head commit's files to a temporary directory and runs oxlint there, with a configuration that sets no plugin and turns every category off, so the consumer's own `.oxlintrc.json`, its ignore files and its other rules never reach the count.
|
|
17
|
+
- `dist/` is committed.
|
|
18
|
+
No `prepack` or `prepublishOnly` builds it, so a publish ships whatever bundle the publishing worktree holds.
|
|
19
|
+
Rebuild it after pulling with `bun run build`.
|
|
20
|
+
`bun run build` also emits `quality.schema.json`, `templates/` and `CHANGELOG.md`, which are committed the same way.
|
|
21
|
+
CI runs `git diff --exit-code` over the whole tree after the build, because a test that compares a generated file with its source passes on the copy the build just rewrote.
|
|
22
|
+
- `CHANGELOG.md` is generated, so a release commit carries it and the tarball ships it.
|
|
23
|
+
The commit that bumps package.json `version` closes its release, and the `v*` tag goes on that commit, so commits merged after it wait for the next release.
|
|
24
|
+
Releases come from the commits, over all of HEAD's ancestry, so a checkout without tags, a fork and a branch that merged `main` in write the same file.
|
|
25
|
+
A bump not newer than the release before it is a revert: it cancels every release above the version it returns to, and a tag only keeps a reverted release that was already published.
|
|
26
|
+
The committed changelog is the record of what was released: a version older than the newest it lists and absent from it was never published, and its commits go into the next release.
|
|
27
|
+
A section keeps the date it was written with, since the squash merge that lands the release commit may fall on another day.
|
|
28
|
+
Entries come from commit subjects, the squash-merged pull request titles commitlint holds to the conventional format; the bodies are the branch's own messages, which nothing lints.
|
|
29
|
+
The release path needs no `contents: write`: the changelog arrives in the release commit's pull request, not from a workflow that pushes.
|
|
30
|
+
- `quality.json` is JSON, not TOML or a TypeScript module: a bun bin, a hook running without `node_modules`, a `.cjs` or `.mjs` config and `jq` all parse it with nothing installed, and nobody runs a repository's own code to learn its policy.
|
|
31
|
+
It holds declarations only.
|
|
32
|
+
The kit's bins read it directly.
|
|
33
|
+
oxlint and tsc read nothing but their own JSON, so they extend generated fragments, which `checks-quality --check` holds to the declarations.
|
|
34
|
+
- `quality.json` refuses a key its schema does not name, so a kit that cannot enforce a newer key refuses it rather than let the repository believe it enforced.
|
|
35
|
+
- The base parses with swc because typescript 7, which is tsgo, has no compiler API for dependency-cruiser to use.
|
|
36
|
+
Without `@swc/core` installed the cruise silently skips every `.ts` file, so this repo's test asserts its own TypeScript is cruised.
|
|
37
|
+
- `bunfig.toml` has no `extends` and no include: bun ignores an unknown top-level key in silence, so a preset cannot be inherited and the consumer's copy is compared key by key against the installed one instead.
|
|
38
|
+
`[test] pathIgnorePatterns` is a real bunfig key, and an empty `--path-ignore-patterns` flag overrides the file's own list.
|
|
39
|
+
- The Stryker preset is a JavaScript module, not JSON: Stryker 10 does not resolve `extends` in a JSON config, but a `.mjs` config that spreads an imported object consumes it.
|
|
40
|
+
Keys the consumer sets after the spread win.
|
|
41
|
+
- The `.ts` bins are written in Effect, so `effect` is a peer dependency and `@effect/platform-bun`, which only the bins use, is a dependency.
|
|
42
|
+
`@effect/platform-node-shared` is a direct dependency at the same exact version only to pin it: `@effect/platform-bun` asks for it with a `^` range, and a newer rc peers on a newer `effect` than consumers install, so all three move together.
|
|
43
|
+
- Each runnable script ships a `checks-` bin entry, so consumer `package.json` scripts call the short name, which the package manager puts on `PATH` only there.
|
|
44
|
+
A shell runs it through `bun run`, which never falls back to the registry the way `bunx` does.
|
|
45
|
+
The `.ts` checks keep a `bun` shebang, which needs no build step and no `dist/` entry, unlike the oxlint plugin that node loads.
|
|
46
|
+
- `checks-lint` runs each gate as its own bin in a child process rather than importing it, so a gate behaves the same called alone or through the entry point, and `lint-coverage.sh` stays a shell script.
|
|
47
|
+
The gates run one at a time with their output passed straight through, so each report reads whole and in the table's order.
|
|
48
|
+
- A gate selection is checked against the repository's contents rather than trusted, so it cannot skip a gate that applies.
|
|
49
|
+
ci-wiring does that check, which is why a selection without it, or without another gate that applies everywhere, is refused as `checks-lint` reads it: nothing would check the selection otherwise.
|
|
50
|
+
- `checks-test` runs bun itself rather than reading a report some other run left: a skip taken only on CI is visible only in CI's own run, and an earlier run's report may be stale or narrowed.
|
|
51
|
+
It reads the JUnit report bun writes to a temporary directory, since bun has no other per-test output meant for a program.
|
|
52
|
+
- `checks-ci-wiring` runs inside `lint`, not in a workflow of its own: deleting the step that runs a check is the violation it catches, so the local `lint` is where it has to fail.
|
|
53
|
+
- Workflows are parsed with `Bun.YAML`, which the `bun` shebang already provides, so the check adds no dependency.
|
|
54
|
+
It reads `on` as a string key, not as the YAML 1.1 boolean.
|
|
55
|
+
- `bun` counts as a built-in module.
|
|
56
|
+
Nothing installed resolves it except `@types/bun`, which would otherwise make every runtime `bun` import look like a dev-only dependency.
|
|
57
|
+
- The pull request merge commit GitHub builds is authored by `GitHub <noreply@github.com>`, which commit-identity refuses as an author.
|
|
58
|
+
`checks-lint` ends a pull request's range at the event's head sha, so the merge commit is never in it.
|
|
59
|
+
A `lint` that calls `checks-commit-identity HEAD` itself checks out `github.event.pull_request.head.sha` instead of the default merge ref.
|
|
60
|
+
- `no-deep-imports` judges the import specifier, never the resolved file.
|
|
61
|
+
The base honours `exports` maps, so a subpath the map publishes resolves and passes, one it omits fails to resolve and is reported, and a package without an `exports` map publishes every file.
|
|
62
|
+
A bare import always passes whatever file its entry lives in.
|
|
63
|
+
Setting your own `options.enhancedResolveOptions` replaces the base's, so restate `exportsFields` and `conditionNames` if you do.
|
|
64
|
+
- dependency-cruiser `extends` merges same-name `forbidden` rules with the child's fields winning.
|
|
65
|
+
That is the entry-point and layer recipe under Boundaries in [The dependency rules](configs/dependency-rules.md).
|
|
66
|
+
- The templates in `templates/` are rendered from `scripts/doc-templates.ts`, the spec `checks-docs` reads.
|
|
67
|
+
A template written by hand beside the check agrees with it only until someone edits one of them.
|
|
68
|
+
- A page's Diátaxis mode is declared in `quality.json` rather than read from the page.
|
|
69
|
+
Whether a page teaches, walks a task, describes or explains is a judgment no program makes, so the repository states it once and the check holds the page to it.
|
|
70
|
+
- `checks-docs` holds a doc file to its template when a change touches it, the way `checks-size-budget` holds a file to its budget.
|
|
71
|
+
A repository adopts the templates as its files change, and an untouched file is listed as advisory rather than failing a change that never read it.
|
|
72
|
+
- A task heading is verb first, and review holds it there rather than the check.
|
|
73
|
+
No word list tells `Test layout` from `Test the layout`, and a check that passes the noun is worse than none.
|
|
74
|
+
|
|
75
|
+
## Related topics
|
|
76
|
+
|
|
77
|
+
- [checks](../README.md)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# checks-backtest
|
|
2
|
+
|
|
3
|
+
`checks-backtest` is the report of what the comment check would have refused at each recent commit, and a reader looks it up to measure a repository's own history before adopting the check.
|
|
4
|
+
|
|
5
|
+
## What it checks
|
|
6
|
+
|
|
7
|
+
It checks nothing and fails nothing.
|
|
8
|
+
It walks recent first-parent commits, prints a row per commit that touches code and then the totals, attributes only the refusals each commit introduced, and counts new comment text as a share of added lines.
|
|
9
|
+
A row holds the commit, its added lines, its added comment lines, its refusals and its subject.
|
|
10
|
+
Each commit that introduced a refusal is then listed with its refusals.
|
|
11
|
+
|
|
12
|
+
## What it reads
|
|
13
|
+
|
|
14
|
+
It reads each commit and its first parent, and the files each commit changes that have a comment syntax in `scripts/comment-matchers.ts`.
|
|
15
|
+
`generated/`, `vendor/`, `repos/`, `node_modules/` and `dist/` are out of reach, so the figures are authored code.
|
|
16
|
+
|
|
17
|
+
## Arguments
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
checks-backtest [commit-count]
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`commit-count` is the number of first-parent commits it walks, 60 when absent.
|
|
24
|
+
|
|
25
|
+
## Exit codes
|
|
26
|
+
|
|
27
|
+
| Code | When |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| 0 | the report printed |
|
|
30
|
+
| 2 | it is given more than one argument, or git cannot read the history, as with a count git refuses |
|
|
31
|
+
|
|
32
|
+
## Sample output
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
fe12189 9 0 0 feat(scripts): hold CONTRIBUTING.md to the how-to template
|
|
36
|
+
086bdd0 1205 1 0 feat(scripts): add checks-docs gate holding doc files to s
|
|
37
|
+
dc23b30 21 1 0 fix: release 0.12.0 and read local exports in checks-featu
|
|
38
|
+
bcb4e3b 1252 4 0 feat(scripts): add size budget, feature-owner rules and ch
|
|
39
|
+
4 commits touching code, 2487 added lines
|
|
40
|
+
comment lines added: 6 (0.2% of added lines)
|
|
41
|
+
refusals introduced: 0
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Opting out
|
|
45
|
+
|
|
46
|
+
Nothing runs it but a person who wants the figures.
|
|
47
|
+
|
|
48
|
+
## Related topics
|
|
49
|
+
|
|
50
|
+
- [checks-comment-gate](checks-comment-gate.md)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# checks-ci-wiring
|
|
2
|
+
|
|
3
|
+
`checks-ci-wiring` is the gate that fails when a command the repository's CI must run no longer runs on pull requests to the default branch, and a reader looks it up to learn which workflow step counts.
|
|
4
|
+
|
|
5
|
+
## What it checks
|
|
6
|
+
|
|
7
|
+
No local check sees a gate drop out of CI, since a workflow whose lint step became a no-op leaves `bun run lint` green.
|
|
8
|
+
The repository declares its gates once, in `quality.json`:
|
|
9
|
+
|
|
10
|
+
```json
|
|
11
|
+
"gates": {
|
|
12
|
+
"ci": ["bun run lint", "bun run typecheck", "bun run test"]
|
|
13
|
+
}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
It looks, for each gate, for a `run:` step that is the gate command alone on one line, optionally followed by plain arguments.
|
|
17
|
+
Plain arguments are words, quoted strings, and `$VAR` or `${VAR}` expansions.
|
|
18
|
+
`bun run lint --quiet` and `bunx checks-comment-gate "origin/$BASE_REF" "$HEAD_SHA"` count.
|
|
19
|
+
`bun run lint:deps`, `echo bun run lint` and a step `name:` do not.
|
|
20
|
+
|
|
21
|
+
A step never counts when its script has a second line or any of these, because each can run the gate without its failure failing the step:
|
|
22
|
+
|
|
23
|
+
- `|`, `||`, `&&`, `;` or `&`
|
|
24
|
+
- `$(...)` or backticks
|
|
25
|
+
- `<` or `>` redirection
|
|
26
|
+
- a comment
|
|
27
|
+
- a leading `NAME=value`
|
|
28
|
+
|
|
29
|
+
The report names such a gate and says to give it its own step with nothing else in it.
|
|
30
|
+
A gate step counts only when all of these hold:
|
|
31
|
+
|
|
32
|
+
- Its workflow triggers on `pull_request`.
|
|
33
|
+
Any `branches` or `branches-ignore` filter there keeps the default branch, any `types` filter keeps `opened` and `synchronize`, and it sets no `paths` or `paths-ignore` filter, which would let some pull requests skip the gate.
|
|
34
|
+
- Neither the step nor its job sets `if: false` or `continue-on-error: true`, bare or as `${{ false }}` and `${{ true }}`.
|
|
35
|
+
- Its job needs no job, directly or through a chain, that sets `if: false`, unless a job on that chain has an `if:` calling `always()`, `failure()` or `cancelled()`.
|
|
36
|
+
GitHub prefixes every other `if:`, including `true` and `success()`, with `success()`, so a job whose needed job was skipped is skipped too.
|
|
37
|
+
|
|
38
|
+
A job calling a local reusable workflow, such as `uses: ./.github/workflows/x.yml`, passes its own trigger and `if:` down to the called workflow's steps.
|
|
39
|
+
A remote reusable workflow, such as `uses: owner/repo/...@ref`, is not a supported way to wire a gate.
|
|
40
|
+
It is not read, so a gate must run as a `run:` step in the repository's own workflows, such as `bunx checks-comment-gate`.
|
|
41
|
+
|
|
42
|
+
A step running `checks-lint` also counts for a declared gate that calls one of the gates `checks-lint` runs by its bare bin name, when the step calls `checks-lint` the same way.
|
|
43
|
+
`bunx checks-lint` counts for `bunx checks-comment-gate "origin/$BASE_REF" "$HEAD_SHA"`.
|
|
44
|
+
A step running `bun run lint` counts only for the `bun run lint` gate, since the check never reads what a package script runs.
|
|
45
|
+
So once `lint` runs `checks-lint`, the per-gate entries can leave `gates.ci` along with the workflows that ran them.
|
|
46
|
+
|
|
47
|
+
A command a schedule must run, such as the flake run, goes in `gates.scheduled`:
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
"gates": {
|
|
51
|
+
"ci": ["bun run lint", "bun run typecheck", "bun run test"],
|
|
52
|
+
"scheduled": ["bunx checks-flake --runs 10 --report flake-report.json"]
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Each counts only as a step of the same plain shape in a workflow whose `on` carries `schedule` with at least one `cron`, under the same `if: false`, `continue-on-error: true` and `needs` rules as a gate.
|
|
57
|
+
|
|
58
|
+
It also checks `gates.lint`, as [Gate selection](checks-lint.md#gate-selection) says.
|
|
59
|
+
|
|
60
|
+
## What it reads
|
|
61
|
+
|
|
62
|
+
It reads the working tree: `quality.json` for `defaultBranch`, `gates.ci`, `gates.scheduled` and `gates.lint`, and every `.github/workflows/*.yml` and `*.yaml`.
|
|
63
|
+
The default branch is `main` when `quality.json` declares none.
|
|
64
|
+
It parses each workflow with `Bun.YAML` and never runs it.
|
|
65
|
+
|
|
66
|
+
## Arguments
|
|
67
|
+
|
|
68
|
+
It takes none.
|
|
69
|
+
|
|
70
|
+
## Exit codes
|
|
71
|
+
|
|
72
|
+
| Code | When |
|
|
73
|
+
| --- | --- |
|
|
74
|
+
| 0 | every declared command runs where it must |
|
|
75
|
+
| 1 | a gate does not run on pull requests to the default branch, a scheduled command runs on no schedule, or `gates.lint` leaves out a gate that applies |
|
|
76
|
+
| 2 | `quality.json` declares no `gates.ci` or does not decode, a gate or scheduled command is not one plain command, or a workflow does not parse |
|
|
77
|
+
|
|
78
|
+
Whether a workflow is well formed is actionlint's question, not this one's.
|
|
79
|
+
|
|
80
|
+
## Sample output
|
|
81
|
+
|
|
82
|
+
It names each gap, with every step that runs the gate and why that step does not count:
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
ci-wiring: 1 of 8 gate(s) do not run on pull requests to main:
|
|
86
|
+
bun run lint
|
|
87
|
+
.github/workflows/release.yml job publish step 7: .github/workflows/release.yml does not trigger on pull_request
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
It names each scheduled command no schedule runs:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
ci-wiring: 1 of 1 scheduled command(s) do not run on a schedule:
|
|
94
|
+
bunx checks-flake --runs 10 --report flake-report.json
|
|
95
|
+
.github/workflows/ci.yml job checks step 5: .github/workflows/ci.yml does not trigger on a schedule
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Opting out
|
|
99
|
+
|
|
100
|
+
It applies to every repository, so no selection leaves it out.
|
|
101
|
+
It runs inside `lint`, through `checks-lint`, because deleting the step that runs a check is the violation it catches:
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
"scripts": {
|
|
105
|
+
"lint": "oxlint --type-aware && checks-lint"
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Limits
|
|
110
|
+
|
|
111
|
+
The check reads workflow files and never runs them, so it deliberately does not evaluate these:
|
|
112
|
+
|
|
113
|
+
- an `if:` expression other than a constant `true` or `false`, which counts as running
|
|
114
|
+
- a `strategy.matrix` `include` or `exclude`, so a matrix that drops every combination still counts as running its steps
|
|
115
|
+
- a remote reusable workflow, whose steps are never read
|
|
116
|
+
- anything that happens at run time on the runner, such as what the gate command itself does, the shell's options, and a step or job that fails or times out before the gate step
|
|
117
|
+
|
|
118
|
+
## Related topics
|
|
119
|
+
|
|
120
|
+
- [checks-lint](checks-lint.md)
|
|
121
|
+
- [checks-flake](checks-flake.md)
|
|
122
|
+
- [The quality file](../configs/quality-file.md)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# checks-comment-gate
|
|
2
|
+
|
|
3
|
+
`checks-comment-gate` is the gate that refuses a banned comment on a line a change adds, and a reader looks it up to learn which comments it refuses.
|
|
4
|
+
|
|
5
|
+
## What it checks
|
|
6
|
+
|
|
7
|
+
It fails when an added line carries a banned comment.
|
|
8
|
+
It refuses a machine-read directive, a record or ticket pointer, a doc block, and a file opening with a rationale block over three lines, licence headers excepted.
|
|
9
|
+
Each refusal says what to write instead.
|
|
10
|
+
Only added lines are checked, so a violation in a file the diff never touches stays silent.
|
|
11
|
+
A refusal counts when any line of the comment carrying it was added.
|
|
12
|
+
|
|
13
|
+
## What it reads
|
|
14
|
+
|
|
15
|
+
It reads the diff between two commits, and the added lines of each file whose extension has a comment syntax in `scripts/comment-matchers.ts`.
|
|
16
|
+
It passes over a file with any other extension.
|
|
17
|
+
|
|
18
|
+
`scripts/comment-matchers.ts` holds the scanner, the comment syntaxes and a synchronous `refused()`, and imports nothing.
|
|
19
|
+
A host such as a hook bundle can therefore copy it alone into a directory with no `node_modules` and import it as `@avi2dg/checks/scripts/comment-matchers.ts`.
|
|
20
|
+
The kit's own dependency cruise fails when that file gains an import.
|
|
21
|
+
`scripts/comments.ts` wraps the same matchers in Effect for the gate and for [checks-backtest](checks-backtest.md).
|
|
22
|
+
|
|
23
|
+
## Arguments
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
checks-comment-gate <base-ref> <head-ref>
|
|
27
|
+
checks-comment-gate <ref>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
With two arguments it diffs the base against the head.
|
|
31
|
+
With one it diffs that commit against its parent, or against the empty tree for a repository's first commit, which has none.
|
|
32
|
+
A shallow checkout running the one-argument form needs `fetch-depth: 2`.
|
|
33
|
+
|
|
34
|
+
## Exit codes
|
|
35
|
+
|
|
36
|
+
| Code | When |
|
|
37
|
+
| --- | --- |
|
|
38
|
+
| 0 | no added line carries a refused comment |
|
|
39
|
+
| 1 | an added line carries a refused comment |
|
|
40
|
+
| 2 | a ref does not resolve, or the parent exists but is not in the clone, which it refuses rather than widening to the whole tree |
|
|
41
|
+
|
|
42
|
+
## Sample output
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
comment-gate: 2 violation(s):
|
|
46
|
+
src/a.ts:1 points at a record or a ticket ("#41"). Drop the pointer: a record is reached by searching docs/adr, and the story of the change goes in the commit message
|
|
47
|
+
src/a.ts:2 carries the machine-read directive `eslint-disable-next-line`. Fix what the tool is reporting, or stop running the tool on this file
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Opting out
|
|
51
|
+
|
|
52
|
+
It applies to every repository, so no selection leaves it out.
|
|
53
|
+
`checks-lint` runs it over each pull request's range, as [checks-lint](checks-lint.md) says.
|
|
54
|
+
|
|
55
|
+
## Related topics
|
|
56
|
+
|
|
57
|
+
- [checks-backtest](checks-backtest.md)
|
|
58
|
+
- [checks-lint](checks-lint.md)
|