@avi2dg/checks 0.26.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +7 -6
  3. package/dist/data-shape/index.js +428 -0
  4. package/dist/templates/reference.md +1 -1
  5. package/docs/configs/commit-messages.md +1 -1
  6. package/docs/configs/dependency-rules.md +7 -1
  7. package/docs/configs/effect-rules.md +10 -0
  8. package/docs/configs/native-settings.md +8 -30
  9. package/docs/configs/typescript-rules.md +43 -1
  10. package/docs/design.md +232 -104
  11. package/docs/gates/checks-advisories.md +128 -0
  12. package/docs/gates/checks-changelog.md +8 -5
  13. package/docs/gates/checks-ci-wiring.md +2 -2
  14. package/docs/gates/checks-comment-gate.md +3 -4
  15. package/docs/gates/checks-commit-identity.md +3 -3
  16. package/docs/gates/checks-docs.md +56 -36
  17. package/docs/gates/checks-exports.md +4 -4
  18. package/docs/gates/checks-flake.md +3 -3
  19. package/docs/gates/checks-lint-coverage.md +17 -9
  20. package/docs/gates/checks-lint.md +9 -4
  21. package/docs/gates/checks-mutation-compare.md +3 -3
  22. package/docs/gates/checks-quarantine-clock.md +3 -5
  23. package/docs/gates/checks-release-notes.md +4 -4
  24. package/docs/gates/checks-release-report.md +3 -3
  25. package/docs/gates/checks-repetition.md +2 -1
  26. package/docs/gates/checks-subsumed-tests.md +3 -3
  27. package/docs/gates/checks-suppressions-ratchet.md +3 -4
  28. package/docs/gates/checks-test-layout.md +4 -4
  29. package/docs/gates/checks-test.md +3 -2
  30. package/docs/gates/checks-unused.md +4 -4
  31. package/docs/gates/checks-vendor.md +3 -3
  32. package/oxlintrc.json +22 -2
  33. package/package.json +16 -6
  34. package/src/complexity/exports.ts +8 -16
  35. package/src/complexity/knip.ts +1 -1
  36. package/src/core/gates.ts +3 -0
  37. package/src/core/git.ts +10 -0
  38. package/src/delivery/ci-wiring.ts +1 -1
  39. package/src/dependencies/advisories.ts +115 -0
  40. package/src/dependencies/advisory-rules.ts +206 -0
  41. package/src/dependencies/cache-root.ts +15 -0
  42. package/src/dependencies/osv-scanner.ts +176 -0
  43. package/src/dependencies/vendor.ts +1 -10
  44. package/src/docs/doc-names.ts +128 -0
  45. package/src/docs/doc-templates.ts +1 -1
  46. package/src/docs/docs.ts +17 -14
  47. package/src/docs/prose-matchers.ts +11 -1
  48. package/src/quality/lint-coverage.sh +35 -2
  49. package/src/quality/presets/effect.language-service.json +3 -1
  50. package/src/testing/mutation-compare.ts +51 -45
  51. package/src/testing/quarantine-clock.ts +2 -11
  52. package/src/testing/test-layout.ts +1 -1
  53. package/ts-reset.d.ts +2 -0
  54. package/tsconfig.effect.json +3 -0
@@ -4,7 +4,7 @@ audience: consumers
4
4
  ---
5
5
  # The TypeScript rules
6
6
 
7
- The oxlint base config holds a repository's TypeScript to a set of rules, and a reader looks it up to learn what each rule refuses and which rules need type information.
7
+ The oxlint base config and the tsconfig fragment hold a repository's TypeScript to a set of rules, and some of them need type information.
8
8
 
9
9
  ## Syntax rules
10
10
 
@@ -33,6 +33,48 @@ Without the flag, oxlint skips them and reports nothing about them.
33
33
  - `typescript/no-unsafe-type-assertion` refuses an `as` that narrows a value to a type the compiler cannot prove.
34
34
  - `typescript/no-deprecated` refuses a use of a symbol whose declaration carries a `@deprecated` tag, in the repository's own code or in a package's types, and repeats the tag's text.
35
35
 
36
+ ## Data-shape rules
37
+
38
+ The base loads the kit's `data-shape` plugin from `dist/` with one rule for every TypeScript file and one for production files.
39
+
40
+ - `data-shape/readonly-collection-param` refuses a parameter typed `T[]`, `Array<T>`, `Map` or `Set` that the function never mutates, stores, returns or passes on, and passing it to a readonly parameter of a function declared in the same file does not count as passing it on.
41
+ - Type such a parameter `readonly T[]`, `ReadonlyArray<T>`, `ReadonlyMap` or `ReadonlySet`.
42
+ - `data-shape/schema-twin` refuses an object type whose fields match a `Schema.Struct`, `TaggedStruct` or `Class` in the same file by name, count, optionality and kind.
43
+ - Derive such a type from the schema with `typeof Name.Type` instead of writing both.
44
+ - The twin rule runs on production files only, so a test that declares its own schema as an oracle stays green.
45
+
46
+ ## Rules outside tests
47
+
48
+ An override in `oxlintrc.json` turns on these type-aware rules in each `.ts` and `.tsx` file outside `tests/`:
49
+
50
+ - `typescript/no-unsafe-assignment` refuses assigning an `any` value to a variable, a property or a destructured name.
51
+ - `typescript/no-unsafe-member-access` refuses reading a member of an `any` value.
52
+ - `typescript/no-unsafe-argument` refuses passing an `any` value to a parameter of another type.
53
+ - `typescript/no-unsafe-return` refuses returning an `any` value from a function, unless the function returns `unknown`.
54
+ - `typescript/no-unsafe-call` refuses calling an `any` value.
55
+
56
+ A consumer inherits the override through `extends`, and a file under `tests/` answers to none of the five.
57
+
58
+ ## Compiler options
59
+
60
+ `tsconfig.effect.json` sets these compiler options in each repository whose `tsconfig.json` extends it:
61
+
62
+ - `erasableSyntaxOnly` refuses TypeScript syntax that does not erase to JavaScript, such as an `enum` or a parameter property.
63
+ - `exactOptionalPropertyTypes` refuses `undefined` as the value of an optional property whose type does not name `undefined`, so a type derived from a `Schema.optionalKey` field accepts only a missing key, as the schema does.
64
+
65
+ ## ts-reset rules
66
+
67
+ `tsconfig.effect.json` lists the kit's `ts-reset.d.ts` in `files`, and that file loads two rules of `@total-typescript/ts-reset`, a dependency of the kit:
68
+
69
+ - `is-array` types the array `Array.isArray` narrows a value to as `unknown[]` rather than `any[]`.
70
+ - `json-parse` types the value `JSON.parse` returns as `unknown` rather than `any`.
71
+
72
+ tsc then refuses code that uses either value as a type it has not checked.
73
+ The fragment also sets `include` to every file under the directory of the repository's `tsconfig.json` and to `ts-reset.d.ts`.
74
+ A repository whose `tsconfig.json` sets `include` or `files` but not both keeps both rules.
75
+ A repository that sets only `files` also gets every file under that directory in its program.
76
+ A repository that sets both `files` and `include` drops both rules, and `checks-lint-coverage` fails it.
77
+
36
78
  ## Related topics
37
79
 
38
80
  - [The Effect rules](effect-rules.md)
package/docs/design.md CHANGED
@@ -3,111 +3,239 @@ kind: explanation
3
3
  ---
4
4
  # Why it is shaped this way
5
5
 
6
- Each entry below is a choice in the kit's shape and the constraint that forced it.
7
-
8
- - 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.
9
- `rules`, `categories` and `jsPlugins` inherit as expected.
10
- That is why the consumer config and each override restate `plugins`.
11
- - `node_modules/` is excluded through the consumer's `.gitignore`, not `ignorePatterns`: oxlint still walks the installed package when only `ignorePatterns` names it.
12
- - `files` in package.json is the published surface: `tests/`, `AGENTS.md` and the `.ts` plugin source never reach an install.
13
- npm adds `package.json`, `README` and `LICENSE` to the tarball whatever `files` says.
14
- `bun pm pack` builds the same tarball the registry serves, which is what the packed-tarball consumer e2e test installs.
15
- - Each plugin ships compiled under `dist/`, built with `bun build src/<vector>/<name>/index.ts --outdir dist/<name> --target node --format esm`.
16
- Node refuses to type-strip a `.ts` plugin under `node_modules`, so the `.ts` source would fail to load from an installed package.
17
- - The source sits under `src/<vector>/`, one directory for each thing the kit judges a repository on: complexity, quality, testing, docs, delivery and dependencies.
18
- `src/core/` holds what every vector runs on, and `scripts/` holds only the kit's own build, which nothing ships.
19
- Sorting by what loads a file put the two oxlint plugins at the root and every bin in one flat `scripts/`, so the files for one purpose sat in several places and nothing said which gate a helper served.
20
- A mutation runner's default scope covers `src/`, so the kit's own Stryker run mutates its source without a `mutate` list.
21
- The testing vector and its directory are named testing rather than tests, since a `src/tests/` beside the root `tests/` would read as a second suite.
22
- An `exports` key a consumer resolves as a specifier keeps pointing at the file's new home, so that specifier resolves as before while a path read without resolution does not.
23
- - The size rules are plain oxlint rules at `error` in `.oxlintrc.json`, and `bun run lint` enforces them on the whole tree.
24
- A repository records its existing violations with `oxlint --suppress-all`, and `checks-suppressions-ratchet` refuses any count that rises.
25
- The kit runs no size script of its own, since restating how oxlint reads its config and compares sites left corners the native rules never had.
26
- The suppression count lets a file or function already over its limit grow without a new site, which was accepted in exchange for dropping that script, and each repository is to work its counts down to zero.
27
- - `checks-repetition` writes both ends of the range to temporary directories and runs jscpd in each with the head's `.jscpd.json`, so its `path` and `ignore` globs decide the files at both ends.
28
- It compares each file's count of repeated lines rather than using jscpd's `--baseline-from-ref`.
29
- That flag reports a repeated block as new once its text changes, so a change that shortens a grandfathered block would fail.
30
- - `dist/` is committed.
31
- No `prepack` or `prepublishOnly` builds it, so a publish ships whatever bundle the publishing worktree holds.
32
- Rebuild it after pulling with `bun run build`.
33
- `bun run build` also emits `dist/templates/` and `CHANGELOG.md`, which are committed the same way.
34
- 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.
35
- - `CHANGELOG.md` is generated, so a release commit carries it and the tarball ships it.
36
- 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.
37
- 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.
38
- 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.
39
- 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.
40
- A section keeps the date it was written with, since the squash merge that lands the release commit may fall on another day.
41
- Entries come from commit subjects, the squash-merged pull request titles commitlint holds to the conventional format.
42
- The bodies are the branch's own messages, which nothing lints.
43
- The changelog still arrives in the release commit's pull request, not from a workflow that pushes.
44
- The release path writes to the repository only through the `github-release` job's `contents: write`, which creates or updates the GitHub release from the tag's `CHANGELOG.md` section.
45
- - Workflow YAML owns CI execution, and the kit owns the required commands, but a repository is held only to the commands its own `package.json` can run.
46
- `checks-ci-wiring` requires `bun run` with each of `lint`, `build`, `typecheck` and `test` that `package.json` defines as a script, `git diff --exit-code` when a `build` script exists, and commitlint always.
47
- The target branch comes from git's own record in `refs/remotes/origin/HEAD`, or in CI from the pull request base or the default branch GitHub's event names, and never from a guess at the workflow files, and `checks-lint` starts its local range from the same branch.
48
- Oxlint and TypeScript read their own overrides directly, so the consumer can change a rule where its tool reads it.
49
- The Effect scopes occur in two native files, and the installed consumer test checks both independently.
50
- - The base parses with swc because typescript 7, which is tsgo, has no compiler API for dependency-cruiser to use.
51
- Without `@swc/core` installed the cruise silently skips every `.ts` file, so this repo's test asserts its own TypeScript is cruised.
52
- - `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.
53
- `[test] pathIgnorePatterns` is a real bunfig key, and an empty `--path-ignore-patterns` flag overrides the file's own list.
54
- - 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.
55
- Keys the consumer sets after the spread win.
56
- - 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.
57
- `@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.
58
- - 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.
59
- A shell runs it through `bun run`, which never falls back to the registry the way `bunx` does.
60
- The `.ts` checks keep a `bun` shebang, which needs no build step and no `dist/` entry, unlike the oxlint plugins that node loads.
61
- - `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.
62
- The gates run one at a time with their output passed straight through, so each report reads whole and in the table's order.
63
- - `checks-lint` determines applicable gates from tracked files instead of accepting a repository selection.
64
- A TypeScript gate starts running as soon as TypeScript source is tracked.
65
- - `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.
66
- It reads the JUnit report bun writes to a temporary directory, since bun has no other per-test output meant for a program.
67
- - `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.
68
- - Workflows are parsed with `Bun.YAML`, which the `bun` shebang already provides, so the check adds no dependency.
69
- It reads `on` as a string key, not as the YAML 1.1 boolean.
70
- - `bun` counts as a built-in module.
71
- Nothing installed resolves it except `@types/bun`, which would otherwise make every runtime `bun` import look like a dev-only dependency.
72
- - The pull request merge commit GitHub builds is authored by `GitHub <noreply@github.com>`, which commit-identity refuses as an author.
73
- `checks-lint` ends a pull request's range at the event's head sha, so the merge commit is never in it.
74
- A `lint` that calls `checks-commit-identity HEAD` itself checks out `github.event.pull_request.head.sha` instead of the default merge ref.
75
- - `no-deep-imports` judges the import specifier, never the resolved file.
76
- 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.
77
- A bare import always passes whatever file its entry lives in.
78
- Setting your own `options.enhancedResolveOptions` replaces the base's, so restate `exportsFields` and `conditionNames` if you do.
79
- - dependency-cruiser `extends` merges same-name `forbidden` rules with the child's fields winning.
80
- That is the entry-point and layer recipe under Boundaries in [The dependency rules](configs/dependency-rules.md).
81
- - The templates in `dist/templates/` are rendered from `src/docs/doc-templates.ts`, the spec `checks-docs` reads.
82
- A template written by hand beside the check agrees with it only until someone edits one of them.
83
- - A page's Diátaxis mode comes from `kind` front matter on the page, whatever directory holds it.
84
- The repository makes the judgment beside the page, and the check holds it to that template.
85
- - `checks-docs` holds a doc file to its template when a change touches it, the way `checks-comment-gate` judges the comments a change adds.
86
- 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.
87
- - A task heading is verb first, and review holds it there rather than the check.
88
- No word list tells `Test layout` from `Test the layout`, and a check that passes the noun is worse than none.
89
- - The prose rules judge only the lines a change adds or edits, the way `checks-comment-gate` judges comments.
90
- Text nobody touched never turns a change red, a record keeps the words it was written in, and a repository needs no cleanup pass before the gate runs.
91
- - A living doc takes one sentence per line, so a changed line is a changed sentence.
92
- Under a hard wrap a one-word edit reflows a paragraph, and the gate would then demand fixes to sentences the edit never touched.
93
- - An agent file such as `AGENTS.md` takes the separator rules and no other prose rule.
94
- One sentence per line serves the people who review a doc's diffs, and an agent file keeps each entry to one line however many sentences it holds.
95
- - `src/docs/prose-matchers.ts` imports nothing, so the gate and a write-time hook run one matcher and refuse in the same words.
96
- A hook bundle ships without `node_modules`, so a matcher that needed Vale or a package could not refuse at write time.
97
- - Readability grades and words such as easy stay out of the prose rules.
98
- A score cannot fail a change without failing correct prose, and a suggestion nobody runs an editor for is never seen.
99
- - A path, link or command on a line the range leaves alone fails when the range broke it, as by deleting the file it names.
100
- A reference goes stale when the code it names moves far more often than when its own line is edited, so a gate on edited lines alone would miss the usual break.
101
- - A path under a top directory the repository lacks names a file in another repository, such as a consumer's, and no program tells that from a typo.
102
- A directory the range deletes still counts as this repository's, so a path under it reads as stale rather than foreign.
103
- - The command check passes over a page whose front matter sets `audience: consumers`.
104
- Such a page speaks to a consuming repository, whose scripts are not this one's.
105
- - `checks-vendor` strips an owner write bit that came back on a cached tree and keeps the tree, rather than refusing it or cloning it again.
106
- The GitHub Actions runner clears the read only mode of each item before it deletes `$RUNNER_TEMP`, and on a directory link that chmod lands on the shared tree's top directory.
107
- Refusing the tree would fail every later job on the runner until a person cleared it, and cloning it again would need the network after every such job.
108
- A write bit is not a write, so the run strips it and then holds the tree to the recorded commit as it holds any tree, and a write it finds there still fails the run.
109
- A group or other write bit still fails the run, since another user could have edited `.git/config` through it before `git status` reads it.
6
+ Most of the kit's shape follows from a limit in a tool it runs on, such as oxlint, bun, npm, Stryker or GitHub Actions.
7
+ Each section names the limit and what the obvious alternative would break.
8
+
9
+ ## Each tool reads its own config
10
+
11
+ A repository keeps each setting in the file its tool reads, such as `.oxlintrc.json`, `tsconfig.json` or `.jscpd.json`.
12
+ A developer then changes a rule where the tool reads it, and the tool's own docs describe that file.
13
+ The Effect paths sit in two of those files, `.oxlintrc.json` and `tsconfig.json`, and the installed consumer test checks each one on its own.
14
+
15
+ oxlint does not pass `plugins` down an `extends` chain.
16
+ A config in the chain that sets no `plugins` gets oxlint's default plugins, and the base's `categories` then turn on their rules across the tree.
17
+ So a consumer's `.oxlintrc.json` and each of its overrides list `plugins` again.
18
+ `rules`, `categories` and `jsPlugins` pass down the chain as expected.
19
+ `.gitignore` keeps oxlint out of `node_modules/`, because oxlint still walks the installed package when only `ignorePatterns` names it.
20
+
21
+ The base turns `data-shape/readonly-collection-param` on for every TypeScript file and `data-shape/schema-twin` on for production files only.
22
+ `typescript/prefer-readonly-parameter-types` would flag every object parameter for deep `readonly`, so the kit rule stays with the collections a function never mutates.
23
+ A test that declares its own schema as an oracle for a production type is reasonable, so the twin rule leaves `tests/` out.
24
+
25
+ bun has no bunfig `extends`, and it ignores an unknown top-level key without a warning.
26
+ So a repository copies the kit's `bunfig.toml`, and `checks-test-layout` compares the copy with the installed one key by key.
27
+ An empty `--path-ignore-patterns` flag overrides the copied `[test] pathIgnorePatterns`, which is how a quarantined test still runs on demand.
28
+
29
+ Stryker 10 does not resolve `extends` in a JSON config.
30
+ So the Stryker preset is a JavaScript module that a repository's `stryker.conf.mjs` spreads, and a key set after the spread wins.
31
+
32
+ The dependency-cruiser base parses with swc, because TypeScript 7, which is tsgo, has no compiler API that dependency-cruiser can use.
33
+ Without `@swc/core` installed, the cruise skips every `.ts` file without a warning, so the kit's own suite asserts that its TypeScript is cruised.
34
+ The base counts `bun` as a built-in module.
35
+ Only `@types/bun` resolves it, and that package is a dev dependency, so every runtime `bun` import would otherwise read as dev only.
36
+
37
+ ## The shared configs close gaps in the types
38
+
39
+ The shared configs refuse four places where a value's type says less than the value does.
40
+ Each sits in a file a consumer already extends or copies.
41
+
42
+ The base `oxlintrc.json` turns on the five `typescript/no-unsafe-*` rules in an override for `.ts` and `.tsx` files outside `tests/`, so every consumer gets them through `extends`.
43
+ `typescript/no-explicit-any` and `strict` already refuse an `any` someone writes, so the `any` left is one nobody wrote.
44
+ `Array.isArray` narrows an `unknown` to `any[]`, `JSON.parse` returns `any`, `Object.entries` lists `any` values from an `object`, and a defaulted parameter in a generator passed to `Effect.fnUntraced` is typed `any`.
45
+ Tests stay out because bun:test types its asymmetric matchers, such as `expect.arrayContaining`, as returning `any`.
46
+ Those matchers made 22 of the 34 findings in the tests of the kit and its consumers.
47
+
48
+ `tsconfig.effect.json` sets `exactOptionalPropertyTypes`, so every repository that extends it gets the option in the same release.
49
+ `Schema.optionalKey` means the key is missing, never `undefined`.
50
+ Without the option, a type derived from the schema accepts an `undefined` the schema rejects when it decodes.
51
+ tsc keeps no baseline, and the only escape for one site is a `@ts-expect-error`, which `typescript/ban-ts-comment` refuses.
52
+ So each error the option raises is fixed where it lands.
53
+
54
+ The language service preset sets `processEnv` and `processEnvInEffect` at error, so code in a repository's Effect paths reads the environment through `Config`.
55
+ `Config` decodes a variable and fails in the error channel when it is missing, where `process.env` hands back a `string | undefined` that each caller checks by hand.
56
+ The language service keeps no baseline either, so each read the two diagnostics find moves to `Config` when a repository takes the preset.
57
+
58
+ `tsconfig.effect.json` loads the `is-array` and `json-parse` rules of `@total-typescript/ts-reset`, so tsc refuses code that relied on the `any` that `Array.isArray` and `JSON.parse` hand out.
59
+ The `no-unsafe-*` rules flag each place that `any` is used, and these two rules close it where it starts, in `tests/` as well.
60
+ The other eight ts-reset rules stay out, because `set-has` breaks correct code in the kit and a consumer, and the rest find nothing.
61
+ A global declaration reaches a program only when a root file, a `types` entry or an import names it, and a repository's own `files`, `include` or `types` replaces the fragment's list of the same name.
62
+ So the fragment lists the rules in `files`, and in `include` beside every file under the repository's `tsconfig.json`, and one of the two lists survives unless a repository sets both.
63
+ `types` cannot carry the rules, since every consumer sets its own `types` to reach `bun`.
64
+ `checks-lint-coverage` fails the repository that sets both lists, because nothing in tsc would say the rules were gone.
65
+
66
+ ## The source is sorted by what it judges
67
+
68
+ The source sits under `src/<vector>/`, one directory for each thing the kit judges a repository on: complexity, quality, testing, docs, delivery and dependencies.
69
+ `src/core/` holds what every vector runs on.
70
+ `scripts/` holds only the kit's own build, and nothing in it ships.
71
+ Sorting files by what loads them would put both oxlint plugins at the root and every bin in one flat directory.
72
+ Nothing would then say which gate a helper serves.
73
+ A mutation runner's default scope covers `src/`, so the kit's own Stryker run mutates its source with no `mutate` list.
74
+ The testing directory is named `testing` rather than `tests`, because a `src/tests/` beside the root `tests/` would read as a second suite.
75
+
76
+ Four `exports` keys name a path the file does not sit at, because consumers resolve them by that name.
77
+ `@avi2dg/checks/scripts/test-skips.ts`, `@avi2dg/checks/scripts/comment-matchers.ts` and `@avi2dg/checks/scripts/prose-matchers.ts` point at their files under `src/`.
78
+ `@avi2dg/checks/templates/*` points at `dist/templates/`.
79
+ A path read without the resolver, such as `node_modules/@avi2dg/checks/scripts/test-skips.ts`, does not exist.
80
+
81
+ ## The package ships built plugins and runnable bins
82
+
83
+ `files` in `package.json` lists what an install gets.
84
+ `tests/`, `AGENTS.md` and the TypeScript source of the oxlint plugins never reach an install.
85
+ npm adds `package.json`, `README.md` and `LICENSE` whatever `files` says.
86
+ `bun pm pack` builds the same tarball the registry serves, and the consumer e2e test installs that tarball.
87
+
88
+ Each oxlint plugin ships compiled under `dist/`, because Node refuses to strip types from a `.ts` file under `node_modules`.
89
+ `dist/` is committed, with the doc templates in `dist/templates/`, and so is `CHANGELOG.md`, which the same build writes.
90
+ No `prepack` or `prepublishOnly` script rebuilds them, so a publish ships the committed files.
91
+ CI runs `git diff --exit-code` over the whole tree after `bun run build`.
92
+ A test that compares a generated file with its source cannot do this job, because it would read the copy the build just rewrote.
93
+
94
+ Each runnable script ships as a `checks-` bin, so a consumer's `package.json` script calls it by the short name.
95
+ The package manager puts a bin on `PATH` only inside a package script.
96
+ A shell reaches it through `bun run`, which never falls back to the registry the way `bunx` does.
97
+ The `.ts` bins keep a `bun` shebang and need no build step, unlike the oxlint plugins that Node loads.
98
+
99
+ The bins are written in Effect.
100
+ So `effect` is a peer dependency, and `@effect/platform-bun`, which only the bins use, is a dependency.
101
+ `@effect/platform-node-shared` is a direct dependency only to pin its version.
102
+ `@effect/platform-bun` asks for it with a `^` range, and a newer release candidate of it peers on a newer `effect` than consumers install.
103
+ So the three packages move together at one exact version.
104
+
105
+ ## checks-lint runs each gate as its own bin
106
+
107
+ `checks-lint` runs each gate in a child process rather than importing it.
108
+ A gate then behaves the same alone or through `checks-lint`, and `lint-coverage.sh` can stay a shell script.
109
+ 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.
110
+ `checks-lint` picks the gates that apply from the tracked files, and a repository cannot select gates.
111
+ A TypeScript gate runs as soon as the repository tracks TypeScript source.
112
+
113
+ GitHub authors the pull request merge commit it builds as `GitHub <noreply@github.com>`, and `checks-commit-identity` refuses that author.
114
+ So `checks-lint` ends a pull request's range at the event's head commit, and the merge commit is never in it.
115
+ A `lint` that calls `checks-commit-identity HEAD` directly checks out `github.event.pull_request.head.sha` rather than the default merge ref.
116
+
117
+ ## Workflows say how CI runs
118
+
119
+ A repository's workflow YAML says how CI runs, and the kit says which commands must run.
120
+ `checks-ci-wiring` holds a repository to title lint on every pull request and to the `bun run` commands its own `package.json` defines, so a repository with no `build` script is not asked to run one.
121
+ The target branch comes from git's `refs/remotes/origin/HEAD`, or in CI from the pull request base or the default branch in GitHub's event.
122
+ It never comes from the workflow files, and `checks-lint` starts its local range from the same branch.
123
+ `checks-ci-wiring` runs inside `lint`, not in a workflow of its own.
124
+ The violation it catches is a deleted workflow step, so it has to fail in the local `lint`.
125
+ It parses workflows with `Bun.YAML`, which the `bun` shebang already provides, so it adds no dependency.
126
+ It reads `on` as a string key, not as the YAML 1.1 boolean.
127
+
128
+ ## Size and repetition only fail on growth
129
+
130
+ The size limits are oxlint's own rules at `error`, not a script of the kit's.
131
+ A script of the kit's own would restate how oxlint reads its config and compares sites, and each restatement adds cases oxlint itself does not have.
132
+ A repository records its existing violations with `oxlint --suppress-all`, and `checks-suppressions-ratchet` refuses any count that rises.
133
+ The cost is that a file or function already over its limit can grow without adding a site the count sees.
134
+ Each repository works its counts down to zero.
135
+
136
+ `checks-repetition` runs jscpd at both ends of the range with the head's `.jscpd.json`, so the same `path` and `ignore` globs pick the files at both ends.
137
+ It compares each file's count of repeated lines, and does not use jscpd's `--baseline-from-ref`.
138
+ That flag reports a repeated block as new once its text changes, so a change that shortens an existing repeated block would fail.
139
+
140
+ ## checks-test runs the suite itself
141
+
142
+ `checks-test` runs bun itself rather than reading a report that another run left.
143
+ A skip taken only on CI shows only in CI's own run, and an earlier run's report may be stale or narrowed.
144
+ It reads the JUnit report bun writes to a temporary directory, because bun has no other per-test output meant for a program.
145
+
146
+ ## Quarantine has one limit
147
+
148
+ `checks-quarantine-clock` holds every quarantined test to one limit of 30 days.
149
+ GitLab quarantines a flaky test for 3 days on its fast path, and for at most 3 months on its long path.
150
+ It then opens a merge request that deletes the test.
151
+ The kit's one limit falls between GitLab's two, so a flaky test gets a month to be fixed, and no path keeps it out of the run for 3 months.
152
+
153
+ ## The changelog comes from the commits
154
+
155
+ `CHANGELOG.md` is generated from the commits, so the release commit carries it and the tarball ships it.
156
+ The commit that bumps `version` in `package.json` closes a release, and the `v*` tag goes on that commit.
157
+ Commits merged after the bump wait for the next release.
158
+ Releases come from the version bumps across all of `HEAD`'s ancestry, not from tags.
159
+ So a checkout without tags, a fork, and a branch that merged `main` in all write the same file when no release was reverted.
160
+ A section keeps the date it was written, because the squash merge that lands the release commit may fall on another day.
161
+ Entries come from commit subjects, which are the squash-merged pull request titles that commitlint holds to the conventional format.
162
+ A commit body holds the branch's own messages, and nothing lints it, so no entry comes from a body.
163
+ The changelog arrives in the release pull request, and no workflow pushes to the repository.
164
+ The only write access the release path holds is the `github-release` job's `contents: write`, which creates or updates the GitHub release from the tag's `CHANGELOG.md` section.
165
+
166
+ ## The docs gate judges what a change touches
167
+
168
+ The templates in `dist/templates/` are rendered from `src/docs/doc-templates.ts`, which is the spec `checks-docs` reads.
169
+ A template written by hand beside the check would agree with it only until someone edits one of them.
170
+ A page's Diátaxis mode comes from `kind` front matter on the page, whatever directory holds it.
171
+ The repository judges the mode beside the page, and the check holds the page to that mode's template.
172
+
173
+ `checks-docs` holds a doc file to its template only when a change touches it, the way `checks-comment-gate` judges only the comments a change adds.
174
+ A repository adopts the templates as its files change, and an untouched file is listed as advisory.
175
+ The prose rules judge only the lines a change adds or edits.
176
+ A report about the past is refused the way a promise about the future is, because history on a living page reads as current fact.
177
+ Text nobody touched never breaks the templates or the prose rules, and a record keeps the words it was written in.
178
+ A repository needs no cleanup pass before the gate runs.
179
+ Review, not the check, keeps a task heading verb first.
180
+ No word list tells `Test layout` from `Test the layout`, and a check that passes the noun would be worse than none.
181
+
182
+ A living doc takes one sentence per line, so a changed line is a changed sentence.
183
+ Under a hard wrap, a one-word edit reflows a paragraph, and the gate would demand fixes to sentences the edit never touched.
184
+ An agent file such as `AGENTS.md` takes the separator rules and the rule against a report about the past, and no other prose rule.
185
+ An agent reads stale history as literally as a person, so the past rule judges agent files too.
186
+ One sentence per line serves the people who review a doc's diffs.
187
+ An agent file keeps each entry on one line, however many sentences it holds.
188
+ The reference check reads agent files too, because a path they name goes stale the same way.
189
+ Readability grades and words such as easy stay out of the prose rules.
190
+ A score cannot fail a change without failing correct prose, and a suggestion that only an editor shows is never seen.
191
+
192
+ `src/docs/prose-matchers.ts` imports nothing, so the gate and a write-time hook run one matcher and refuse in the same words.
193
+ A hook bundle ships without `node_modules`, so a matcher that needed Vale or another package could not refuse at write time.
194
+
195
+ A path, link or command on a line the range leaves alone still fails when the range broke it, for example by deleting the file it names.
196
+ A reference goes stale far more often because the code it names moves than because its own line is edited.
197
+ So a gate on edited lines alone would miss the usual break.
198
+ A name that is not a path goes stale the same way, and the reference check never reads it.
199
+ So the range that removes such a name from every file outside the docs fails on each line that still carries it.
200
+ A path under a top directory the repository lacks names a file in another repository, such as a consumer's.
201
+ No program can tell such a path from a typo.
202
+ A directory the range deletes still counts as this repository's, so a path under it reads as stale rather than foreign.
203
+ The command check passes over a page whose front matter sets `audience: consumers`, because such a page names a consuming repository's scripts.
204
+
205
+ ## checks-vendor keeps a tree whose write bit came back
206
+
207
+ The GitHub Actions runner clears the read only mode of each item before it deletes `$RUNNER_TEMP`.
208
+ On a directory link, that change lands on the shared tree's top directory.
209
+ Refusing the tree would fail every later job on the runner until a person cleared it.
210
+ Cloning the tree again would need the network after every such job.
211
+ A write bit is not a write.
212
+ So `checks-vendor` strips an owner write bit and checks the tree against its recorded commit like any other tree.
213
+ A write it finds there still fails the run.
214
+ A group or other write bit fails the run, because another user could have edited `.git/config` through it before `git status` reads it.
215
+
216
+ ## Advisories fail only the range that adds them
217
+
218
+ `checks-advisories` compares the advisories at both ends of a range instead of failing on every advisory at the head.
219
+ An advisory published against a package that landed a month earlier would otherwise fail every open pull request, including one that touches only docs.
220
+ The scheduled `--all` run finds those advisories, and `advisory-acks.json` carries the ones a repository accepts for a while.
221
+
222
+ The gate runs OSV-Scanner rather than Trivy, Grype or `bun audit`.
223
+ Trivy 0.74.0 reads a nested `bun.lock` entry such as `mkdirp/minimist` under a name no advisory carries, so it misses every package bun nests.
224
+ Grype 0.119.0 drops the dev dependencies of `bun.lock` with no setting that keeps them.
225
+ `bun audit` asks the npm registry on every run and has no offline mode.
226
+ OSV.dev's npm export also holds OpenSSF's reports of malicious packages, which GitHub's reviewed advisories leave out.
227
+
228
+ The kit pins the scanner by version and by the SHA-256 of each build, because a scanner release is code that runs on every runner.
229
+ Trivy's own advisory GHSA-69fq-xp46-6x23 records a malicious release published with stolen credentials.
230
+ The scan runs offline against a database refreshed once a day, so most runs need no network.
231
+
232
+ The acknowledgement file belongs to the kit rather than to `osv-scanner.toml`.
233
+ OSV-Scanner's `ignoreUntil` accepts any day, such as 2099-01-01, and measures it against the clock of the machine that runs the scan.
234
+ The kit caps each entry at 30 days and measures a range from the head's dates, as `checks-quarantine-clock` does, so a commit gets the same verdict on every run.
235
+ `--all` measures from the current time instead, because a head's dates never move in a repository that takes no commit, and an entry measured from them would never expire.
236
+ The file is JSON because `Bun.TOML` cannot parse a TOML date.
110
237
 
111
238
  ## Related topics
112
239
 
113
240
  - [checks](../README.md)
241
+ - [The dependency rules](configs/dependency-rules.md)
@@ -0,0 +1,128 @@
1
+ ---
2
+ kind: reference
3
+ audience: consumers
4
+ ---
5
+ # checks-advisories
6
+
7
+ `checks-advisories` is the gate that fails a range whose `bun.lock` change adds a package version with a known security advisory.
8
+
9
+ ## What it checks
10
+
11
+ It scans `bun.lock` at both ends of the range with OSV-Scanner and fails on each advisory the head's lockfile has and the base's lockfile lacks.
12
+ It matches an advisory across the range by package name and the id OSV-Scanner reports, so a range that moves a package between two affected versions adds nothing.
13
+ It ignores aliases, because two live advisories can list each other as aliases.
14
+ An advisory published against a package the base already held shows at both ends, so it fails no range, and `--all` reports it instead.
15
+ Each failure names the package, its version, the advisory id, its severity and its summary.
16
+ A range that leaves `bun.lock` unchanged runs no scan.
17
+ It also holds `advisory-acks.json` to the rules in [The acknowledgement file](#the-acknowledgement-file) on every run.
18
+
19
+ ## What it reads
20
+
21
+ It reads `bun.lock` at the base and at the head of the range from git, not from the working tree.
22
+ It scans copies of both in a scratch directory with an empty config, so a repository's own `osv-scanner.toml` takes no part.
23
+ It reads the advisories OSV.dev exports for npm, which hold GitHub's reviewed advisories and OpenSSF's reports of malicious packages.
24
+
25
+ It runs OSV-Scanner 2.6.0, pinned by the SHA-256 of each platform's build.
26
+ On first use it downloads the build from the scanner's GitHub release into `~/.cache/avi2dg-checks/osv-scanner/2.6.0/`.
27
+ It checks the SHA-256 again on every run and exits 2 on a copy that differs.
28
+ Builds are pinned for macOS and Linux, each on x64 and arm64.
29
+ On any other platform it exits 2, and no setting runs a scanner other than the pinned build.
30
+
31
+ It scans offline against OSV-Scanner's npm database in `~/.cache/avi2dg-checks/osv-scanner/db/`.
32
+ When the last refresh is more than 24 hours old, the scan asks for the database again, and OSV-Scanner downloads it only when the copy differs.
33
+ A last refresh dated ahead of the clock counts as no refresh.
34
+ When that download fails, it scans the cached copy and says so, as long as that copy was refreshed within 7 days.
35
+ With no copy refreshed within 7 days it exits 2.
36
+ A cold cache downloads about 55 MB of scanner and 217 MB of database.
37
+
38
+ `--all` appends its report to the file `GITHUB_STEP_SUMMARY` names, which is the job summary.
39
+
40
+ ## The acknowledgement file
41
+
42
+ `advisory-acks.json` at the repository root lists the advisories the repository accepts for a while, such as a false positive or a fix that waits on an upstream release:
43
+
44
+ ```json
45
+ [
46
+ {
47
+ "package": "minimist",
48
+ "id": "GHSA-xvch-5gv4-984h",
49
+ "until": "2026-10-20",
50
+ "reason": "mkdirp 0.5.1 never parses untrusted argv here, and leaves with the next test runner"
51
+ }
52
+ ]
53
+ ```
54
+
55
+ It reads the file at the head of the range.
56
+ Each entry names the package, the advisory's id as OSV-Scanner reports it, the day the entry stops holding, and why the repository accepts the advisory.
57
+ An entry covers only the advisory with that id, so one naming an alias covers nothing.
58
+ An entry holds until its `until` day begins in UTC.
59
+ A range measures from the later of the head's author and committer dates, so a commit gets the same verdict on every run.
60
+ `--all` measures from the current time, so an entry expires in a repository that takes no commit.
61
+ An entry whose `until` falls more than 30 days after that moment fails the run and covers nothing, and no setting raises the limit.
62
+ Once that moment passes `until`, the entry fails every run until it is deleted or renewed with a later `until`, whether or not the range touches `bun.lock`.
63
+ Delete it once the package is upgraded.
64
+ When a scan runs, an entry that matches no advisory at the head fails, so the file holds only live entries.
65
+ A file that does not decode as that list exits 2.
66
+
67
+ ## Arguments
68
+
69
+ ```sh
70
+ checks-advisories <base-ref> <head-ref>
71
+ checks-advisories <ref>
72
+ checks-advisories --all
73
+ ```
74
+
75
+ With two arguments it judges the range from their merge base to the head.
76
+ With one it judges that commit against its parent, or against an empty tree for a repository's first commit.
77
+ With `--all` it fails on every advisory in `bun.lock` at `HEAD` that no entry covers.
78
+
79
+ ## Exit codes
80
+
81
+ | Code | When |
82
+ | --- | --- |
83
+ | 0 | the range adds no advisory to `bun.lock`, and every acknowledgement holds |
84
+ | 1 | the range adds an advisory no acknowledgement covers, or an acknowledgement does not hold |
85
+ | 2 | a ref does not resolve, `advisory-acks.json` does not decode, or no verified scanner or usable database is at hand |
86
+
87
+ ## Sample output
88
+
89
+ ```
90
+ advisories: the range adds 2 advisory(ies) to bun.lock (0 at the head predate the range, 0 acknowledged); upgrade each package, or acknowledge its advisory in advisory-acks.json:
91
+ lodash@4.17.20 GHSA-35jh-r3h4-6jhm high: Command Injection in lodash
92
+ minimist@0.0.8 GHSA-xvch-5gv4-984h critical: Prototype Pollution in minimist
93
+ advisories: 1 acknowledgement(s) in advisory-acks.json do not hold:
94
+ qs GHSA-4mjr-xmp4-gh2g expired on 2026-10-20; upgrade the package and delete the entry, or renew it with a later day
95
+ ```
96
+
97
+ ## When it runs
98
+
99
+ `checks-lint` runs it over each pull request's range in a repository that tracks `bun.lock`, as [checks-lint](checks-lint.md) says.
100
+ A range that leaves `bun.lock` unchanged runs in under a second, and one that changes it scans for about 10 seconds on a warm cache.
101
+
102
+ ## Running it on a schedule
103
+
104
+ A range never fails on an advisory published after its package landed, so a scheduled `--all` run finds those:
105
+
106
+ ```yaml
107
+ on:
108
+ schedule:
109
+ - cron: "41 4 * * *"
110
+ workflow_dispatch:
111
+ jobs:
112
+ advisories:
113
+ runs-on: self-hosted
114
+ timeout-minutes: 10
115
+ steps:
116
+ - uses: actions/checkout@v5
117
+ - uses: oven-sh/setup-bun@v2
118
+ - run: bun install --frozen-lockfile
119
+ - run: ./node_modules/.bin/checks-advisories --all
120
+ ```
121
+
122
+ A self-hosted runner keeps `~/.cache/avi2dg-checks/` between runs, so it downloads the scanner once per pinned version and the database about once a day.
123
+ A hosted runner starts each run with an empty cache, so each run downloads both.
124
+
125
+ ## Related topics
126
+
127
+ - [checks-lint](checks-lint.md)
128
+ - [Why it is shaped this way](../design.md)
@@ -22,9 +22,12 @@ A version with no conventional commit worth listing writes no section.
22
22
 
23
23
  ## What it reads
24
24
 
25
- It reads `package.json`, `CHANGELOG.md` and the git history from the repository root.
26
- It finds releases in the version bumps of `package.json` across all of `HEAD` ancestry, so a checkout without tags writes the same file.
25
+ It reads `package.json`, `CHANGELOG.md`, the `v*` tags and the git history from the repository root.
26
+ It finds releases in the version bumps of `package.json` across all of `HEAD` ancestry, so a checkout without tags writes the same file when no release was reverted.
27
27
  It reads a commit whose `package.json` is missing or has no version as unversioned, so the commit that adds the version is the first release and a repository that adopts a version late still writes its changelog.
28
+ A bump to a version no newer than the release before it is a revert.
29
+ A revert cancels every release above the version it returns to, except a release a `v` tag marks as published.
30
+ A version older than the newest in the committed `CHANGELOG.md` and absent from it was never published, so its commits go into the next release.
28
31
  It refuses a shallow checkout, since the releases reach back past its history.
29
32
  It refuses a `package.json` with no repository address, since each entry links its pull request under it.
30
33
  It refuses a repository address that is no `https` address once `git+`, a trailing slash and `.git` are dropped, since a pull request link needs one.
@@ -51,10 +54,10 @@ Run it through the build, as the release workflow in [checks-release-notes](chec
51
54
  checks-changelog: wrote 2 release(s) to CHANGELOG.md
52
55
  ```
53
56
 
54
- ## Opting out
57
+ ## When it runs
55
58
 
56
- Nothing runs it but the build of a repository that keeps a changelog.
57
- A repository with no versioned releases leaves it out.
59
+ Only the build of a repository that keeps a changelog runs it.
60
+ A repository with no versioned releases does not need it.
58
61
 
59
62
  ## Related topics
60
63