@avi2dg/checks 0.36.0 → 0.38.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 (35) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +33 -20
  3. package/dependency-cruiser.config.js +92 -60
  4. package/dist/presets/dependency-cruiser.d.ts +15 -0
  5. package/dist/presets/dependency-cruiser.js +126 -0
  6. package/dist/presets/knip.d.ts +6 -0
  7. package/dist/presets/knip.js +15 -0
  8. package/dist/presets/oxlint.d.ts +33 -0
  9. package/dist/presets/oxlint.js +145 -0
  10. package/docs/configs/dependency-rules.md +28 -31
  11. package/docs/configs/effect-rules.md +47 -23
  12. package/docs/configs/native-settings.md +65 -8
  13. package/docs/configs/typescript-rules.md +7 -7
  14. package/docs/design.md +25 -8
  15. package/docs/gates/checks-effect-scope.md +63 -0
  16. package/docs/gates/checks-exports.md +5 -5
  17. package/docs/gates/checks-imports.md +64 -0
  18. package/docs/gates/checks-lint.md +1 -1
  19. package/docs/gates/checks-mutation-baseline.md +99 -0
  20. package/docs/gates/checks-mutation-compare.md +5 -2
  21. package/docs/gates/checks-mutation.md +2 -2
  22. package/docs/gates/checks-unused.md +4 -4
  23. package/oxlintrc.json +49 -10
  24. package/package.json +29 -7
  25. package/src/complexity/exports.ts +1 -1
  26. package/src/complexity/knip.ts +1 -1
  27. package/src/core/gates.ts +4 -0
  28. package/src/dependencies/imports.ts +94 -0
  29. package/src/dependencies/kit-defaults.ts +3 -0
  30. package/src/quality/effect-scope.ts +116 -0
  31. package/src/quality/jsonc-patch.ts +209 -0
  32. package/src/quality/presets/dependency-cruiser.ts +148 -0
  33. package/src/quality/presets/oxlint.ts +165 -0
  34. package/src/testing/mutation-baseline.ts +211 -0
  35. package/src/quality/presets/effect.oxlint.json +0 -11
@@ -0,0 +1,145 @@
1
+ // src/quality/presets/oxlint.ts
2
+ import { fileURLToPath } from "node:url";
3
+ var SIZE_RULES = ["max-lines", "max-lines-per-function", "max-statements", "readability/cognitive-complexity", "max-depth"];
4
+ var SOURCE_LIMITS = {
5
+ "max-lines": 400,
6
+ "max-lines-per-function": 100,
7
+ "max-statements": 30,
8
+ "readability/cognitive-complexity": 15,
9
+ "max-depth": 4
10
+ };
11
+ var TEST_LIMITS = {
12
+ "max-lines": 600,
13
+ "max-lines-per-function": "off",
14
+ "max-statements": 50,
15
+ "readability/cognitive-complexity": 15,
16
+ "max-depth": 4
17
+ };
18
+ var SOURCES = ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"];
19
+ var TESTS = ["tests/**", "**/*.test.ts", "**/*.test.tsx"];
20
+ var PLUGINS = ["typescript", "oxc", "eslint", "import"];
21
+ var EFFECT_ONLY_PLUGINS = ["node", "promise", "unicorn"];
22
+ var EFFECT_PLUGINS = [...PLUGINS, ...EFFECT_ONLY_PLUGINS];
23
+ var JS_PLUGINS = ["dist/effect-channel/index.js", "dist/readability/index.js", "dist/data-shape/index.js"];
24
+ var EFFECT_RULES = {
25
+ "node/no-sync": "error",
26
+ "oxc/no-async-await": "error",
27
+ "promise/avoid-new": "error",
28
+ "unicorn/no-process-exit": "error",
29
+ "effect-channel/no-throw": "error",
30
+ "effect-channel/no-try-catch": "error"
31
+ };
32
+ var LINE_RULES = new Set(["max-lines", "max-lines-per-function"]);
33
+ var REFUSED_EFFECT = "@avi2dg/checks/oxlint: set effect to true, false, or { files, excludeFiles } with at least one glob in files";
34
+ function packagePath(path) {
35
+ return fileURLToPath(new URL(`../../${path}`, import.meta.url));
36
+ }
37
+ var BASE = {
38
+ plugins: [...PLUGINS],
39
+ jsPlugins: JS_PLUGINS.map(packagePath),
40
+ categories: { correctness: "error", suspicious: "error" },
41
+ rules: {
42
+ "effect-channel/no-error-channel-escape": "error",
43
+ "typescript/no-explicit-any": "error",
44
+ "typescript/ban-ts-comment": ["error", { "ts-expect-error": true }],
45
+ "typescript/no-inferrable-types": "error",
46
+ "typescript/explicit-module-boundary-types": "error",
47
+ "typescript/switch-exhaustiveness-check": "error",
48
+ "typescript/prefer-readonly": "error",
49
+ "typescript/no-unnecessary-condition": ["error", { allowConstantLoopConditions: true }],
50
+ "typescript/no-unnecessary-type-parameters": "error",
51
+ "typescript/use-unknown-in-catch-callback-variable": "error",
52
+ "eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_", ignoreRestSiblings: true }],
53
+ "typescript/no-unsafe-type-assertion": "error",
54
+ "typescript/no-non-null-assertion": "error",
55
+ "typescript/no-deprecated": "error",
56
+ "typescript/consistent-return": "off"
57
+ },
58
+ overrides: [
59
+ {
60
+ files: ["**/*.ts", "**/*.tsx"],
61
+ rules: { "data-shape/readonly-collection-param": "error" }
62
+ },
63
+ {
64
+ files: ["**/*.ts", "**/*.tsx"],
65
+ excludeFiles: ["tests/**"],
66
+ rules: {
67
+ "data-shape/schema-twin": "error",
68
+ "typescript/no-unsafe-assignment": "error",
69
+ "typescript/no-unsafe-member-access": "error",
70
+ "typescript/no-unsafe-argument": "error",
71
+ "typescript/no-unsafe-return": "error",
72
+ "typescript/no-unsafe-call": "error"
73
+ }
74
+ },
75
+ {
76
+ files: ["**/*.astro"],
77
+ rules: { "readability/thin-astro": "error", "import/no-unassigned-import": "off" }
78
+ }
79
+ ]
80
+ };
81
+ var base = BASE;
82
+ function sizeSetting(rule, limit) {
83
+ if (limit === "off")
84
+ return "off";
85
+ return LINE_RULES.has(rule) ? ["error", { max: limit, skipBlankLines: false, skipComments: false }] : ["error", { max: limit }];
86
+ }
87
+ function sizeBudget(files, limits, excludeFiles = []) {
88
+ const rules = Object.fromEntries(SIZE_RULES.map((rule) => [rule, sizeSetting(rule, limits[rule])]));
89
+ return { files: [...files], excludeFiles: [...excludeFiles], rules };
90
+ }
91
+ function effectRules(files, excludeFiles = []) {
92
+ return { files: [...files], excludeFiles: [...excludeFiles], plugins: [...EFFECT_PLUGINS], rules: { ...EFFECT_RULES } };
93
+ }
94
+ function isGlobList(value) {
95
+ return Array.isArray(value) && value.every((glob) => typeof glob === "string" && glob !== "");
96
+ }
97
+ function effectOverrides(effect) {
98
+ if (effect === true)
99
+ return [effectRules(SOURCES, TESTS)];
100
+ if (effect === false)
101
+ return [];
102
+ if (typeof effect !== "object" || effect === null)
103
+ throw new TypeError(REFUSED_EFFECT);
104
+ const files = "files" in effect ? effect.files : undefined;
105
+ const excludeFiles = "excludeFiles" in effect ? effect.excludeFiles : [];
106
+ if (!isGlobList(excludeFiles))
107
+ throw new TypeError(REFUSED_EFFECT);
108
+ if (files === undefined)
109
+ return [effectRules(SOURCES, [...TESTS, ...excludeFiles])];
110
+ if (!isGlobList(files) || files.length === 0)
111
+ throw new TypeError(REFUSED_EFFECT);
112
+ return [effectRules(files, excludeFiles)];
113
+ }
114
+ function withRulePlugins(override) {
115
+ const rules = Object.keys(override.rules ?? {});
116
+ const namesEffectOnlyRule = rules.some((rule) => EFFECT_ONLY_PLUGINS.some((plugin) => rule.startsWith(`${plugin}/`)));
117
+ if (!namesEffectOnlyRule)
118
+ return override;
119
+ return { ...override, plugins: [...new Set([...EFFECT_PLUGINS, ...override.plugins ?? []])] };
120
+ }
121
+ function defineConfig({ effect, rules, overrides = [], options, ...rest }) {
122
+ return {
123
+ ...BASE,
124
+ ...rest,
125
+ options: { typeAware: true, ...options },
126
+ rules: { ...BASE.rules, ...rules },
127
+ overrides: [
128
+ ...BASE.overrides,
129
+ sizeBudget(SOURCES, SOURCE_LIMITS, TESTS),
130
+ sizeBudget(TESTS, TEST_LIMITS),
131
+ ...effectOverrides(effect),
132
+ ...overrides.map(withRulePlugins)
133
+ ]
134
+ };
135
+ }
136
+ export {
137
+ SOURCES,
138
+ SOURCE_LIMITS,
139
+ TESTS,
140
+ TEST_LIMITS,
141
+ base,
142
+ defineConfig,
143
+ effectRules,
144
+ sizeBudget
145
+ };
@@ -4,34 +4,40 @@ audience: consumers
4
4
  ---
5
5
  # The dependency rules
6
6
 
7
- The shared dependency-cruiser base holds a repository's imports to a set of rules, and a repository adds its own boundaries on top.
7
+ The kit's dependency-cruiser rules hold a repository's imports, and a repository adds its own boundaries on top.
8
8
 
9
9
  ## Base rules
10
10
 
11
- `.dependency-cruiser.cjs` extends the shared base, which carries these rules:
11
+ `defineConfig` from `@avi2dg/checks/dependency-cruiser` returns these rules, with or without a config of the repository's own:
12
12
 
13
13
  - `no-circular`
14
- - `no-orphans`
14
+ - `no-orphans`, which a repository that depends on `astro` gets only by listing its own, as [checks-imports](../gates/checks-imports.md) explains
15
15
  - `not-to-dev-dep`, which refuses shipped source importing a dev-only package, and a package listed in `peerDependencies` too is not dev-only
16
16
  - `no-non-package-json`, which refuses an import of an installed package that the nearest `package.json` does not declare, and counts a peer that `peerDependenciesMeta` marks optional as declared
17
17
  - `not-to-unresolvable`, which refuses a specifier nothing installed answers
18
18
  - `no-deep-imports`, which refuses a subpath the package's exports map does not publish
19
19
 
20
- The base parses with swc, so it needs `@swc/core` installed, and without it the cruise silently skips every `.ts` file.
20
+ A test file, a config file and every path under `tests/` may import a dev-only package.
21
+
22
+ The rules parse with swc, so the cruise needs `@swc/core` installed, and without it the cruise silently skips every `.ts` file.
21
23
 
22
24
  `no-deep-imports` judges the import specifier, never the file it resolves to.
23
- The base honours `exports` maps, so a subpath the map publishes resolves and passes, and a subpath it omits fails to resolve and is reported.
25
+ The rules honour `exports` maps, so a subpath the map publishes resolves and passes, and a subpath it omits fails to resolve and is reported.
24
26
  A package without an `exports` map publishes every file.
25
27
  A bare import always passes, whatever file its entry lives in.
26
- A repository that sets its own `options.enhancedResolveOptions` replaces the base's, and restates `exportsFields` and `conditionNames` in it.
28
+ A repository that sets its own `options.enhancedResolveOptions` replaces the kit's, and restates `exportsFields` and `conditionNames` in it.
29
+
30
+ ## A repository's own config
27
31
 
28
- ## Boundaries
32
+ A repository writes `dependency-cruiser.config.ts` when the defaults do not fit:
29
33
 
30
- A repository appends a named rule per boundary it owns:
34
+ ```ts
35
+ import { defineConfig } from "@avi2dg/checks/dependency-cruiser";
31
36
 
32
- ```js
33
- module.exports = {
34
- extends: "./node_modules/@avi2dg/checks/dependency-cruiser.config.js",
37
+ export default defineConfig({
38
+ // A launcher script loads the bin by path, so nothing imports it.
39
+ orphans: ["^src/bin[.]ts$"],
40
+ devOnly: ["^(?:evals|tests)/"],
35
41
  forbidden: [
36
42
  {
37
43
  name: "ui-cannot-reach-server",
@@ -40,33 +46,24 @@ module.exports = {
40
46
  to: { path: "^src/server" },
41
47
  },
42
48
  ],
43
- };
49
+ options: { exclude: { path: ["^dist/"] } },
50
+ });
44
51
  ```
45
52
 
46
- A rule that restates a base name overrides it field by field.
47
- That is how an entry point stops being an orphan: redeclare `no-orphans` with the entry added to its `pathNot`.
48
- A repository with an import boundary writes its rule directly under `forbidden`.
53
+ The builder takes dependency-cruiser's own config without `extends`, plus two keys of its own:
49
54
 
50
- ## Running it
55
+ - `devOnly` lists the paths that may import a dev dependency, and replaces the default `["^tests/"]`.
56
+ - `orphans` lists the entry points `no-orphans` passes over.
51
57
 
52
- `package.json` gains the script:
58
+ A rule under `forbidden` that takes a kit rule's name replaces that rule whole, and any other rule is added after the kit's.
59
+ `options.exclude` joins the kit's `^repos/`, which keeps the cruise out of the libraries `checks-vendor` links, and every other option replaces the kit's of the same name.
60
+ `tsc` refuses an `extends` key and a severity dependency-cruiser does not know.
53
61
 
54
- ```json
55
- "lint:deps": "depcruise --config .dependency-cruiser.cjs src"
56
- ```
57
-
58
- CI runs it beside the other checks:
62
+ ## Running it
59
63
 
60
- ```yaml
61
- jobs:
62
- lint:
63
- steps:
64
- - uses: actions/checkout@v5
65
- - uses: oven-sh/setup-bun@v2
66
- - run: bun install --frozen-lockfile
67
- - run: bun run lint:deps
68
- ```
64
+ `checks-lint` runs [checks-imports](../gates/checks-imports.md), which cruises every tracked TypeScript file against `dependency-cruiser.config.ts`, or against the kit's defaults when the repository holds no config.
69
65
 
70
66
  ## Related topics
71
67
 
68
+ - [checks-imports](../gates/checks-imports.md)
72
69
  - [Why it is shaped this way](../design.md)
@@ -8,38 +8,59 @@ The oxlint base and Effect language service check paths a repository writes with
8
8
 
9
9
  ## Oxlint override
10
10
 
11
- The shared `oxlintrc.json` loads `effect-channel/no-error-channel-escape` across the tree.
12
- The repo's `.oxlintrc.json` owns its Effect paths and exemptions in an override:
11
+ The kit's oxlint `base` loads `effect-channel/no-error-channel-escape` across the tree.
12
+ `effect` in `oxlint.config.ts` names the paths that hold the six Effect rules, and has no default:
13
13
 
14
- ```json
14
+ | `effect` | The Effect rules hold |
15
+ | --- | --- |
16
+ | `true` | every source file outside the tests |
17
+ | `false` | no file |
18
+ | `{ excludeFiles }` | every source file outside the tests, less these globs |
19
+ | `{ files, excludeFiles }` | these globs, less `excludeFiles`, with at least one glob in `files` |
20
+
21
+ `files` and `excludeFiles` read as they do in an oxlint override.
22
+ Every source file is `SOURCES`, the `.ts`, `.tsx`, `.mts` and `.cts` files, and the tests are `TESTS`, the files under `tests/` and each `*.test.ts` or `*.test.tsx`.
23
+ A comment beside an excluded glob says why that path stays outside Effect:
24
+
25
+ ```ts
26
+ import { defineConfig } from "@avi2dg/checks/oxlint";
27
+
28
+ export default defineConfig({
29
+ effect: {
30
+ excludeFiles: [
31
+ // Launchd runs the renamer, and it cannot import Effect v4.
32
+ "home/.config/renamer/**",
33
+ ],
34
+ },
35
+ });
36
+ ```
37
+
38
+ A config that leaves `effect` out, sets it to a string or gives `files` no glob fails `tsc`, and oxlint refuses it again when it loads the file.
39
+ `effectRules(files, excludeFiles)` from the same module returns the override itself:
40
+
41
+ ```ts
15
42
  {
16
- "extends": ["./node_modules/@avi2dg/checks/oxlintrc.json"],
17
- "plugins": ["typescript", "oxc", "eslint", "import"],
18
- "overrides": [{
19
- "files": ["src/**/*.ts"],
20
- "excludeFiles": ["src/host/*.ts"],
21
- "plugins": ["typescript", "oxc", "eslint", "import", "node", "promise", "unicorn"],
22
- "rules": {
23
- "node/no-sync": "error",
24
- "oxc/no-async-await": "error",
25
- "promise/avoid-new": "error",
26
- "unicorn/no-process-exit": "error",
27
- "effect-channel/no-throw": "error",
28
- "effect-channel/no-try-catch": "error"
29
- }
30
- }]
43
+ files: ["src/**/*.ts"],
44
+ excludeFiles: ["src/host/*.ts"],
45
+ plugins: ["typescript", "oxc", "eslint", "import", "node", "promise", "unicorn"],
46
+ rules: {
47
+ "node/no-sync": "error",
48
+ "oxc/no-async-await": "error",
49
+ "promise/avoid-new": "error",
50
+ "unicorn/no-process-exit": "error",
51
+ "effect-channel/no-throw": "error",
52
+ "effect-channel/no-try-catch": "error"
53
+ }
31
54
  }
32
55
  ```
33
56
 
34
- The kit ships the rule block in `src/quality/presets/effect.oxlint.json` for copying into the override.
35
- Each config in an oxlint `extends` chain sets `plugins` explicitly, because an omitted list enables defaults across the chain.
36
57
  `unicorn/no-process-exit` does not check a shebang script, so a bin can use `eslint/no-restricted-properties` for `process.exit`.
37
58
 
38
59
  ## Language service
39
60
 
40
61
  The repository's `tsconfig.json` holds its Effect override under `compilerOptions.plugins`.
41
- The override includes the same source paths and excludes the same exempt paths as oxlint.
42
- The kit ships severity values in `src/quality/presets/effect.language-service.json` for the override's `options`.
62
+ [checks-effect-scope](../gates/checks-effect-scope.md) writes that override from `oxlint.config.ts`, so it includes and excludes the same paths as oxlint.
63
+ The override takes its `options` from the severity values the kit ships in `src/quality/presets/effect.language-service.json`.
43
64
  The preset turns these diagnostics to errors:
44
65
 
45
66
  - `nodeBuiltinImport` refuses an import of a Node built-in module that has an Effect counterpart.
@@ -50,9 +71,12 @@ The preset turns these diagnostics to errors:
50
71
  - `processEnvInEffect` refuses a read of `process.env` inside an Effect generator.
51
72
 
52
73
  Effect's `Config` reads the environment in place of `process.env`.
53
- The kit's `tsconfig.effect.json` keeps the shared language service diagnostics.
74
+ The kit's `tsconfig.effect.json` holds the shared language service diagnostics.
75
+ A plugin entry in the repository's `tsconfig.json` stands in for the kit's, so `checks-effect-scope` writes those severities into it on every run.
76
+ The kit owns the severities of its own keys, and a repository adds keys of its own but does not change the kit's.
54
77
 
55
78
  ## Related topics
56
79
 
57
80
  - [The TypeScript rules](typescript-rules.md)
58
81
  - [Native settings](native-settings.md)
82
+ - [checks-effect-scope](../gates/checks-effect-scope.md)
@@ -11,15 +11,15 @@ A consuming repository puts each setting in the file its tool reads, and no mani
11
11
  | File | Setting | Reader |
12
12
  | --- | --- | --- |
13
13
  | `.github/workflows/*.yml` | Pull request commands, scheduled commands, runner labels | GitHub Actions and `checks-ci-wiring` |
14
- | `.oxlintrc.json` | Effect paths, exemptions, file size, function size, statements, cognitive complexity and depth | oxlint |
14
+ | `oxlint.config.ts` | Effect paths and their exemptions, ignore patterns, and any rule or budget the repository sets over the kit's defaults | oxlint and `checks-effect-scope` |
15
15
  | `oxlint-suppressions.json` | The existing violations oxlint suppresses, per file and rule | oxlint and `checks-suppressions-ratchet` |
16
16
  | `exports-baseline.json` | The existing unused exports and types, per file, kind and name | `checks-exports` |
17
17
  | `.jscpd.json` | The `path` and `ignore` globs of the files repetition is measured in | jscpd and `checks-repetition` |
18
- | `knip.config.ts` | The `entry` globs Knip traces unreferenced files from, spread over the kit's `knip-base.json` | Knip, `checks-unused` and `checks-exports` |
19
- | `tsconfig.json` | Effect language service scope and severity | TypeScript and Effect language service |
18
+ | `knip.config.ts` | The `entry` globs Knip traces unreferenced files from | Knip, `checks-unused` and `checks-exports` |
19
+ | `dependency-cruiser.config.ts` | Entry points nothing imports, paths that may import dev dependencies, and import boundaries, when the kit's defaults do not fit | `checks-imports` |
20
+ | `tsconfig.json` | Compiler options, and the Effect language service plugin whose `overrides` and kit severities `checks-effect-scope` writes, keeping every other key | TypeScript and Effect language service |
20
21
  | `package.json` | `scripts` with the `checks-vendor` arguments in `prepare`, `author` and `contributors` | Bun, `checks-commit-identity` and `checks-vendor` |
21
22
  | `bunfig.toml` | Test discovery and quarantine exclusion | Bun and `checks-test-layout` |
22
- | `.dependency-cruiser.cjs` | Import rules | dependency-cruiser |
23
23
  | `stryker.conf.mjs` | Mutation settings | Stryker |
24
24
  | Each page under `docs/` | Diátaxis mode in `kind` front matter, and `audience: consumers` on a page that speaks to a consuming repository | `checks-docs` |
25
25
 
@@ -27,16 +27,66 @@ Every page under `docs/` names its mode in `kind` front matter, whatever directo
27
27
  A page whose front matter sets `audience: consumers` names commands that a consuming repository runs.
28
28
  `checks-docs` skips the `bun run` commands on such a page, and resolves those in every other living doc or agent file as [Paths, links and commands](../gates/checks-docs.md#paths-links-and-commands) says.
29
29
 
30
+ ## The config builders
31
+
32
+ The kit ships a `defineConfig` for oxlint, Knip and dependency-cruiser.
33
+ Each takes the tool's own config type, less the keys the kit owns, and returns the whole config with a default for every kit rule.
34
+ A key only the repository knows has no default, so `tsc` refuses a config that leaves it out.
35
+ The builder throws when the tool loads such a config anyway.
36
+
37
+ | Builder | Required | The kit owns |
38
+ | --- | --- | --- |
39
+ | `@avi2dg/checks/oxlint` | `effect` | `extends`, `plugins`, `jsPlugins` and `categories` |
40
+ | `@avi2dg/checks/knip` | `entry`, where `[]` means the package scripts and tests name every entry | `include` |
41
+ | `@avi2dg/checks/dependency-cruiser` | nothing | `extends` |
42
+
43
+ A repository whose sources are Effect programs writes `oxlint.config.ts`:
44
+
45
+ ```ts
46
+ import { defineConfig } from "@avi2dg/checks/oxlint";
47
+
48
+ export default defineConfig({
49
+ effect: true,
50
+ ignorePatterns: ["src/crawler-extract.js"],
51
+ });
52
+ ```
53
+
54
+ `ignorePatterns`, `rules`, `overrides`, `settings`, `env`, `globals` and `options` mean what they mean in oxlint's own docs.
55
+ The kit's rules come first, and the repository's `rules` and `overrides` land after them.
56
+ An override that sets a `node/`, `promise/` or `unicorn/` rule gets the Effect override's `plugins`, since the kit loads those plugins only there.
57
+ [The Effect rules](effect-rules.md) lists what `effect` accepts.
58
+ `defineConfig` sets `options.typeAware`, so a plain `oxlint` runs the type-aware rules.
59
+ The module also exports the blocks the config is built from: `base`, `sizeBudget`, `effectRules`, `SOURCE_LIMITS`, `TEST_LIMITS`, `SOURCES` and `TESTS`.
60
+
61
+ `knip.config.ts` names the files nothing imports:
62
+
63
+ ```ts
64
+ import { defineConfig } from "@avi2dg/checks/knip";
65
+
66
+ export default defineConfig({ entry: ["src/index.ts"] });
67
+ ```
68
+
69
+ `dependency-cruiser.config.ts` is optional, since `checks-imports` cruises against the kit's defaults without one:
70
+
71
+ ```ts
72
+ import { defineConfig } from "@avi2dg/checks/dependency-cruiser";
73
+
74
+ export default defineConfig({ devOnly: ["^(?:evals|tests)/"] });
75
+ ```
76
+
77
+ [The dependency rules](dependency-rules.md) lists its keys.
78
+ `tsconfig.json` keeps the three config files in its program, so `bun run typecheck` holds them to the builders' types.
79
+
30
80
  ## Size limits
31
81
 
32
- The size rules are oxlint's own rules at `error` in `.oxlintrc.json`, so `bun run lint` fails on any file over them.
82
+ The size rules are oxlint's own rules at `error`, which every `oxlint.config.ts` built with `defineConfig` sets over the whole tree, so `bun run lint` fails on any file over them.
33
83
  A repository records its existing violations with `oxlint --suppress-all`, which writes them to `oxlint-suppressions.json`.
34
84
  `checks-suppressions-ratchet` refuses any count in that file that rises.
35
- The kit's own `.oxlintrc.json` sets these limits for each size override, and a repository may copy them:
85
+ `SOURCE_LIMITS` holds the limits for sources and `TEST_LIMITS` the limits for tests:
36
86
 
37
- <!-- generated size-limits: bun run build writes it from .oxlintrc.json, SIZE_RULES in src/complexity/size-rules.ts and scripts/doc-blocks.ts -->
87
+ <!-- generated size-limits: bun run build writes it from SOURCE_LIMITS, TEST_LIMITS, SOURCES and TESTS in src/quality/presets/oxlint.ts and scripts/doc-blocks.ts -->
38
88
 
39
- | Limits | oxlint rule | `src/**/*.ts`, `scripts/**/*.ts` | `tests/**/*.ts` |
89
+ | Limits | oxlint rule | `**/*.ts`, `**/*.tsx`, `**/*.mts`, `**/*.cts` outside the tests | `tests/**`, `**/*.test.ts`, `**/*.test.tsx` |
40
90
  | --- | --- | --- | --- |
41
91
  | The most lines a file may hold, blank and comment lines counted | `max-lines` | 400 | 600 |
42
92
  | The most lines a function may span, blank and comment lines counted | `max-lines-per-function` | 100 | off |
@@ -46,9 +96,16 @@ The kit's own `.oxlintrc.json` sets these limits for each size override, and a r
46
96
 
47
97
  <!-- end generated size-limits -->
48
98
 
99
+ A file a repository holds to another limit gets an ordinary override in `oxlint.config.ts`:
100
+
101
+ ```ts
102
+ overrides: [{ files: ["src/lib/server.ts"], rules: { "max-lines": ["error", { max: 600 }] } }],
103
+ ```
104
+
49
105
  ## Related topics
50
106
 
51
107
  - [The Effect rules](effect-rules.md)
108
+ - [The dependency rules](dependency-rules.md)
52
109
  - [checks-ci-wiring](../gates/checks-ci-wiring.md)
53
110
  - [checks-suppressions-ratchet](../gates/checks-suppressions-ratchet.md)
54
111
  - [checks-docs](../gates/checks-docs.md)
@@ -4,11 +4,11 @@ audience: consumers
4
4
  ---
5
5
  # The TypeScript rules
6
6
 
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.
7
+ The kit's oxlint base 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
 
11
- `oxlintrc.json` turns on the `correctness` and `suspicious` categories as errors, and these rules on top of them:
11
+ `base` from `@avi2dg/checks/oxlint`, which every config its `defineConfig` returns starts from, turns on the `correctness` and `suspicious` categories as errors, and these rules on top of them:
12
12
 
13
13
  - `typescript/no-explicit-any` refuses an `any` type written out.
14
14
  - `typescript/ban-ts-comment` refuses `@ts-ignore`, `@ts-expect-error` and `@ts-nocheck`.
@@ -22,8 +22,8 @@ It turns on `effect-channel/no-error-channel-escape` as well, and [The Effect ru
22
22
 
23
23
  ## Type-aware rules
24
24
 
25
- These rules read the types, so they run only under `oxlint --type-aware` with `oxlint-tsgolint` installed.
26
- Without the flag, oxlint skips them and reports nothing about them.
25
+ These rules read the types, so they run only with `oxlint-tsgolint` installed and type-aware linting on.
26
+ `defineConfig` turns it on through `options.typeAware`, so a plain `oxlint` runs them.
27
27
 
28
28
  - `typescript/switch-exhaustiveness-check` refuses a `switch` over a union that leaves a member without a case.
29
29
  - `typescript/prefer-readonly` refuses a private member that nothing reassigns and that is not `readonly`.
@@ -45,7 +45,7 @@ The base loads the kit's `data-shape` plugin from `dist/` with one rule for ever
45
45
 
46
46
  ## Astro rules
47
47
 
48
- An override in `oxlintrc.json` turns on one rule of the kit's `readability` plugin in each `.astro` file:
48
+ An override in `base` turns on one rule of the kit's `readability` plugin in each `.astro` file:
49
49
 
50
50
  - `readability/thin-astro` refuses a statement in the frontmatter or a script block that is neither an import, a re-export from another module, a type or interface declaration, nor a variable read from `Astro.props`.
51
51
  - A default inside an `Astro.props` destructuring passes only when it is a literal, a `-` or `+` on a numeric literal, or a name read from `Astro.props` earlier, in the same pattern or a statement before it, so `const { title = "Home", heading = title } = Astro.props;` passes and a call or `await` in a default is refused.
@@ -55,7 +55,7 @@ An override in `oxlintrc.json` turns on one rule of the kit's `readability` plug
55
55
 
56
56
  ## Rules outside tests
57
57
 
58
- An override in `oxlintrc.json` turns on these type-aware rules in each `.ts` and `.tsx` file outside `tests/`:
58
+ An override in `base` turns on these type-aware rules in each `.ts` and `.tsx` file outside `tests/`:
59
59
 
60
60
  - `typescript/no-unsafe-assignment` refuses assigning an `any` value to a variable, a property or a destructured name.
61
61
  - `typescript/no-unsafe-member-access` refuses reading a member of an `any` value.
@@ -63,7 +63,7 @@ An override in `oxlintrc.json` turns on these type-aware rules in each `.ts` and
63
63
  - `typescript/no-unsafe-return` refuses returning an `any` value from a function, unless the function returns `unknown`.
64
64
  - `typescript/no-unsafe-call` refuses calling an `any` value.
65
65
 
66
- A consumer inherits the override through `extends`, and a file under `tests/` answers to none of the five.
66
+ Every config `defineConfig` returns carries the override, and a file under `tests/` answers to none of the five.
67
67
 
68
68
  ## Compiler options
69
69
 
package/docs/design.md CHANGED
@@ -8,14 +8,25 @@ Each section names the limit and what the obvious alternative would break.
8
8
 
9
9
  ## Each tool reads its own config
10
10
 
11
- A repository keeps each setting in the file its tool reads, such as `.oxlintrc.json`, `tsconfig.json` or `.jscpd.json`.
11
+ A repository keeps each setting in the file its tool reads, such as `oxlint.config.ts`, `tsconfig.json` or `.jscpd.json`.
12
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.
13
+
14
+ oxlint, Knip and dependency-cruiser each read a TypeScript config, so the kit ships a `defineConfig` for each over the tool's own config type.
15
+ A repository writes the shape that tool's docs describe, and adds to the kit's rules through the tool's own keys.
16
+ Each builder sets a default for every kit rule, and every glob in it covers the whole tree, so no unlisted directory escapes a rule.
17
+ A rule set copied by hand would miss whatever the copy left out, and would fail on a path the copy forgot.
18
+ A fact only the repository knows has no default, `effect` for oxlint and `entry` for Knip.
19
+ Defaulting `effect` on fails a repository whose code is deliberately outside Effect, and defaulting it off leaves an Effect repository unchecked, so its type is required.
20
+ Neither oxlint nor Knip typechecks the config it loads, so each builder also throws when it gets a config without that fact.
21
+ Both tools read a config's default export synchronously, so a throw is the one refusal open to the builder.
22
+
23
+ The Effect paths sit in `oxlint.config.ts` alone.
24
+ `tsconfig.json` cannot import a TypeScript module, so `checks-effect-scope` writes the Effect language service override in `tsconfig.json` from them, and fails `checks-lint` when the two differ.
14
25
 
15
26
  oxlint does not pass `plugins` down an `extends` chain.
16
27
  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.
28
+ So `defineConfig` returns the whole root config with `plugins` set, and a repository adds its own keys rather than extending a kit object.
29
+ oxlint reads a relative `jsPlugins` path against the consumer's config, so `base` names each plugin bundle by its absolute path in the installed kit.
19
30
  `.gitignore` keeps oxlint out of `node_modules/`, because oxlint still walks the installed package when only `ignorePatterns` names it.
20
31
 
21
32
  The base turns `data-shape/readonly-collection-param` on for every TypeScript file and `data-shape/schema-twin` on for production files only.
@@ -29,9 +40,9 @@ An empty `--path-ignore-patterns` flag overrides the copied `[test] pathIgnorePa
29
40
  Stryker 10 does not resolve `extends` in a JSON config.
30
41
  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
42
 
32
- The dependency-cruiser base parses with swc, because TypeScript 7, which is tsgo, has no compiler API that dependency-cruiser can use.
43
+ The dependency-cruiser rules parse with swc, because TypeScript 7, which is tsgo, has no compiler API that dependency-cruiser can use.
33
44
  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.
45
+ The rules count `bun` as a built-in module.
35
46
  Only `@types/bun` resolves it, and that package is a dev dependency, so every runtime `bun` import would otherwise read as dev only.
36
47
 
37
48
  ## The shared configs close gaps in the types
@@ -39,7 +50,7 @@ Only `@types/bun` resolves it, and that package is a dev dependency, so every ru
39
50
  The shared configs refuse four places where a value's type says less than the value does.
40
51
  Each sits in a file a consumer already extends or copies.
41
52
 
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`.
53
+ The oxlint `base` turns on the five `typescript/no-unsafe-*` rules in an override for `.ts` and `.tsx` files outside `tests/`, so every config `defineConfig` returns carries them.
43
54
  `typescript/no-explicit-any` and `strict` already refuse an `any` someone writes, so the `any` left is one nobody wrote.
44
55
  `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
56
  Tests stay out because bun:test types its asymmetric matchers, such as `expect.arrayContaining`, as returning `any`.
@@ -86,7 +97,8 @@ A path read without the resolver, such as `node_modules/@avi2dg/checks/scripts/t
86
97
  npm adds `package.json`, `README.md` and `LICENSE` whatever `files` says.
87
98
  `bun pm pack` builds the same tarball the registry serves, and the consumer e2e test installs that tarball.
88
99
 
89
- Each oxlint plugin ships compiled under `dist/`, because Node refuses to strip types from a `.ts` file under `node_modules`.
100
+ Each oxlint plugin and each config builder ships compiled under `dist/`, because Node refuses to strip types from a `.ts` file under `node_modules`.
101
+ oxlint loads `oxlint.config.ts` under Node, so the builders ship as JavaScript beside a `.d.ts` that `tsc` reads.
90
102
  `@oxlint/plugins` ships no RuleTester, so each `effect-channel`, `readability` and `data-shape` rule is proven red and green against an installed consumer in `tests/e2e/consumer.test.ts`, except `readability/thin-astro`, which `tests/e2e/astro-consumer.test.ts` proves against a consumer tree linked to the checkout.
91
103
  `dist/` is committed, with the doc templates in `dist/templates/`, and so is `CHANGELOG.md`, which the same build writes.
92
104
  No `prepack` or `prepublishOnly` script rebuilds them, so a publish ships the committed files.
@@ -112,6 +124,11 @@ The gates run one at a time and pass their output straight through, so each repo
112
124
  `checks-lint` picks the gates that apply from the tracked files, and a repository cannot select gates.
113
125
  A TypeScript gate runs as soon as the repository tracks TypeScript source, and `checks-lint-coverage` and `checks-unused` also run on tracked Astro source.
114
126
 
127
+ dependency-cruiser runs as the `checks-imports` gate rather than as a script each repository adds to `lint`, so no repository can leave it out.
128
+ A repository with no config of its own is cruised against the kit's defaults, since every rule has one.
129
+ The gate cruises the tracked TypeScript, so a build output or a linked library outside git never reaches the cruise.
130
+ It runs dependency-cruiser under Bun, which loads a TypeScript config wherever it sits, the kit's defaults under `node_modules` among them.
131
+
115
132
  GitHub authors the pull request merge commit it builds as `GitHub <noreply@github.com>`, and `checks-commit-identity` refuses that author.
116
133
  So `checks-lint` ends a pull request's range at the event's head commit, and the merge commit is never in it.
117
134
  A `lint` that calls `checks-commit-identity HEAD` directly checks out `github.event.pull_request.head.sha` rather than the default merge ref.
@@ -0,0 +1,63 @@
1
+ ---
2
+ kind: reference
3
+ audience: consumers
4
+ ---
5
+ # checks-effect-scope
6
+
7
+ `checks-effect-scope` writes the Effect paths of `oxlint.config.ts` and the kit's severities into the Effect language service plugin in `tsconfig.json`.
8
+ As a gate it fails when either differs from what it would write.
9
+
10
+ ## What it checks
11
+
12
+ It loads `oxlint.config.ts` and takes the `files` and `excludeFiles` of the override holding the six Effect rules, the one the `effect` key of `defineConfig` builds.
13
+ Each such override becomes one entry under `overrides` of the `@effect/language-service` plugin in `tsconfig.json`, with `files` as its `include`, `excludeFiles` as its `exclude`, and the severities in `src/quality/presets/effect.language-service.json` as its `options`.
14
+ The `overrides` of that plugin and the kit's keys in its `diagnosticSeverity` belong to it, and every other key of `tsconfig.json` stays as the repository wrote it.
15
+ With `effect: false` the plugin holds no `overrides`.
16
+ With `--check` it fails when the override or the kit's severities in `tsconfig.json` differ from what it would write, and writes nothing.
17
+
18
+ ## What it reads
19
+
20
+ It reads `oxlint.config.ts` and `tsconfig.json` at the repository root.
21
+ It reads `tsconfig.json` as TypeScript does, so a comment or a trailing comma parses, and refuses one that does not parse as a JSONC object.
22
+ It rewrites only the `overrides` value of the `@effect/language-service` plugin and the kit's keys in its `diagnosticSeverity`, as JSON with two spaces of indent.
23
+ When that value is missing it adds the member, and the plugin, `plugins` or `compilerOptions` holding it, after the last member of each.
24
+ Each key of the `diagnosticSeverity` in the kit's `tsconfig.effect.json` gets the kit's value, and a key the repository added stays.
25
+ A key a release drops from `tsconfig.effect.json` stays in the repository's entry until the repository removes it.
26
+ It refuses a `compilerOptions` or `plugins` set to `null`, or a `diagnosticSeverity` that is not an object, which would leave no place for what it writes.
27
+ With `effect: false` it deletes the `overrides` member and its comma, and leaves the comments beside it.
28
+ Every other byte of `tsconfig.json` stays as the repository wrote it.
29
+ A repository without `tsconfig.json` has no language service to hold the paths, so it passes.
30
+
31
+ ## Arguments
32
+
33
+ ```sh
34
+ checks-effect-scope
35
+ checks-effect-scope --check
36
+ ```
37
+
38
+ With no argument it writes `tsconfig.json`.
39
+ With `--check` it only compares.
40
+
41
+ ## Exit codes
42
+
43
+ | Code | When |
44
+ | --- | --- |
45
+ | 0 | `tsconfig.json` holds the Effect paths and the kit's severities, or was written to hold them |
46
+ | 1 | with `--check`, `tsconfig.json` holds other Effect paths or other severities for the kit's keys |
47
+ | 2 | `oxlint.config.ts` does not load, or `tsconfig.json` does not parse, sets `compilerOptions` or `plugins` to `null`, or holds a `diagnosticSeverity` that is not an object |
48
+
49
+ ## Sample output
50
+
51
+ ```
52
+ effect-scope: the @effect/language-service entry in tsconfig.json differs from the Effect paths of oxlint.config.ts or the kit's severities; run checks-effect-scope to rewrite them
53
+ ```
54
+
55
+ ## When it runs
56
+
57
+ A repository runs `checks-effect-scope` from its `build` script, so CI's `git diff --exit-code` after the build fails on a stale `tsconfig.json` too.
58
+ `checks-lint` runs it with `--check` when the repository tracks `oxlint.config.ts`.
59
+
60
+ ## Related topics
61
+
62
+ - [The Effect rules](../configs/effect-rules.md)
63
+ - [checks-lint](checks-lint.md)