@shivaedev/quality 0.21.0 → 0.22.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 (125) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +24 -8
  3. package/biome/declarations.json +1 -1
  4. package/biome/preset.json +1 -1
  5. package/dist/biome/exceptionConfig.d.ts +7 -0
  6. package/dist/biome/exceptionConfig.d.ts.map +1 -0
  7. package/dist/biome/exceptionConfig.js +54 -0
  8. package/dist/biome/exceptionConfig.js.map +1 -0
  9. package/dist/biome/report.d.ts +2 -1
  10. package/dist/biome/report.d.ts.map +1 -1
  11. package/dist/biome/report.js +5 -5
  12. package/dist/biome/report.js.map +1 -1
  13. package/dist/biome/run.d.ts +2 -1
  14. package/dist/biome/run.d.ts.map +1 -1
  15. package/dist/biome/run.js +5 -4
  16. package/dist/biome/run.js.map +1 -1
  17. package/dist/cli/args.d.ts +5 -1
  18. package/dist/cli/args.d.ts.map +1 -1
  19. package/dist/cli/args.js +6 -27
  20. package/dist/cli/args.js.map +1 -1
  21. package/dist/cli/fix.d.ts.map +1 -1
  22. package/dist/cli/fix.js +9 -9
  23. package/dist/cli/fix.js.map +1 -1
  24. package/dist/cli/lint.d.ts.map +1 -1
  25. package/dist/cli/lint.js +2 -1
  26. package/dist/cli/lint.js.map +1 -1
  27. package/dist/cli/messages.d.ts +3 -0
  28. package/dist/cli/messages.d.ts.map +1 -0
  29. package/dist/cli/messages.js +30 -0
  30. package/dist/cli/messages.js.map +1 -0
  31. package/dist/cli/migrateExceptions.d.ts +4 -0
  32. package/dist/cli/migrateExceptions.d.ts.map +1 -0
  33. package/dist/cli/migrateExceptions.js +78 -0
  34. package/dist/cli/migrateExceptions.js.map +1 -0
  35. package/dist/cli.js +3 -0
  36. package/dist/cli.js.map +1 -1
  37. package/dist/config/decode.d.ts +6 -0
  38. package/dist/config/decode.d.ts.map +1 -1
  39. package/dist/config/decode.js +2 -0
  40. package/dist/config/decode.js.map +1 -1
  41. package/dist/config/load.d.ts +4 -0
  42. package/dist/config/load.d.ts.map +1 -1
  43. package/dist/config/load.js +10 -1
  44. package/dist/config/load.js.map +1 -1
  45. package/dist/config/resolve.d.ts.map +1 -1
  46. package/dist/config/resolve.js +11 -1
  47. package/dist/config/resolve.js.map +1 -1
  48. package/dist/config.d.ts +2 -0
  49. package/dist/config.d.ts.map +1 -1
  50. package/dist/config.js.map +1 -1
  51. package/dist/engine/run-rules.d.ts +1 -0
  52. package/dist/engine/run-rules.d.ts.map +1 -1
  53. package/dist/engine/run-rules.js +19 -4
  54. package/dist/engine/run-rules.js.map +1 -1
  55. package/dist/engine/session.d.ts.map +1 -1
  56. package/dist/engine/session.js +11 -3
  57. package/dist/engine/session.js.map +1 -1
  58. package/dist/exceptions/biomeFindings.d.ts +4 -0
  59. package/dist/exceptions/biomeFindings.d.ts.map +1 -0
  60. package/dist/exceptions/biomeFindings.js +14 -0
  61. package/dist/exceptions/biomeFindings.js.map +1 -0
  62. package/dist/exceptions/declaration.d.ts +11 -0
  63. package/dist/exceptions/declaration.d.ts.map +1 -0
  64. package/dist/exceptions/declaration.js +24 -0
  65. package/dist/exceptions/declaration.js.map +1 -0
  66. package/dist/exceptions/migrateBiome.d.ts +3 -0
  67. package/dist/exceptions/migrateBiome.d.ts.map +1 -0
  68. package/dist/exceptions/migrateBiome.js +62 -0
  69. package/dist/exceptions/migrateBiome.js.map +1 -0
  70. package/dist/exceptions/migrateConfig.d.ts +3 -0
  71. package/dist/exceptions/migrateConfig.d.ts.map +1 -0
  72. package/dist/exceptions/migrateConfig.js +57 -0
  73. package/dist/exceptions/migrateConfig.js.map +1 -0
  74. package/dist/exceptions/registryEntries.d.ts +4 -0
  75. package/dist/exceptions/registryEntries.d.ts.map +1 -0
  76. package/dist/exceptions/registryEntries.js +6 -0
  77. package/dist/exceptions/registryEntries.js.map +1 -0
  78. package/dist/rule.d.ts +3 -0
  79. package/dist/rule.d.ts.map +1 -1
  80. package/dist/rule.js.map +1 -1
  81. package/dist/rules/biome.js +2 -2
  82. package/dist/rules/biome.js.map +1 -1
  83. package/dist/rules/files/namedAfterExport.d.ts.map +1 -1
  84. package/dist/rules/files/namedAfterExport.js +9 -1
  85. package/dist/rules/files/namedAfterExport.js.map +1 -1
  86. package/dist/rules/suppressions/biome/ConsumerDeclaration.d.ts +7 -0
  87. package/dist/rules/suppressions/biome/ConsumerDeclaration.d.ts.map +1 -0
  88. package/dist/rules/suppressions/biome/ConsumerDeclaration.js +15 -0
  89. package/dist/rules/suppressions/biome/ConsumerDeclaration.js.map +1 -0
  90. package/dist/rules/suppressions/biome-overrides.d.ts.map +1 -1
  91. package/dist/rules/suppressions/biome-overrides.js +7 -16
  92. package/dist/rules/suppressions/biome-overrides.js.map +1 -1
  93. package/docs/debt-and-hooks.md +31 -11
  94. package/docs/modules.md +2 -0
  95. package/docs/naming.md +1 -1
  96. package/docs/north-star.md +4 -4
  97. package/docs/roadmap.md +1 -1
  98. package/docs/rules.md +12 -33
  99. package/docs/tooling.md +2 -2
  100. package/package.json +1 -1
  101. package/src/biome/exceptionConfig.ts +70 -0
  102. package/src/biome/report.ts +11 -5
  103. package/src/biome/run.ts +17 -10
  104. package/src/cli/args.ts +7 -29
  105. package/src/cli/fix.ts +14 -9
  106. package/src/cli/lint.ts +3 -1
  107. package/src/cli/messages.ts +31 -0
  108. package/src/cli/migrateExceptions.ts +87 -0
  109. package/src/cli.ts +3 -0
  110. package/src/config/decode.ts +2 -0
  111. package/src/config/load.ts +15 -1
  112. package/src/config/resolve.ts +12 -1
  113. package/src/config.ts +2 -0
  114. package/src/engine/run-rules.ts +21 -4
  115. package/src/engine/session.ts +11 -3
  116. package/src/exceptions/biomeFindings.ts +20 -0
  117. package/src/exceptions/declaration.ts +38 -0
  118. package/src/exceptions/migrateBiome.ts +78 -0
  119. package/src/exceptions/migrateConfig.ts +64 -0
  120. package/src/exceptions/registryEntries.ts +10 -0
  121. package/src/rule.ts +3 -0
  122. package/src/rules/biome.ts +2 -2
  123. package/src/rules/files/namedAfterExport.ts +9 -1
  124. package/src/rules/suppressions/biome/ConsumerDeclaration.ts +24 -0
  125. package/src/rules/suppressions/biome-overrides.ts +7 -26
package/docs/rules.md CHANGED
@@ -49,7 +49,7 @@ A repository with existing comments adopts the rules through the baseline, for e
49
49
 
50
50
  ### Suppressions
51
51
 
52
- A check that is silenced at one site hides the problem instead of fixing it. Five rules close the escape hatches, and none of them takes registry exceptions: a registry entry that names one fails the gate as stale. A repository with existing suppressions adopts the rules through the baseline.
52
+ A check that is silenced at one site hides the problem instead of fixing it. Five rules close the escape hatches, and none of them takes exceptions: an exception that names one fails the gate as stale. A repository with existing suppressions adopts the rules through the baseline.
53
53
 
54
54
  | Rule | Reports | Options |
55
55
  | --- | --- | --- |
@@ -79,7 +79,9 @@ A type test proves that an API rejects what its types forbid, and TypeScript ass
79
79
 
80
80
  #### Declared Biome overrides
81
81
 
82
- Consumer declarations cover one lint rule or assist action on explicit file paths, with a reason. Globs, whole checks and rule groups cannot be declared. A `files/includes` declaration must retain `**` and add only negative patterns naming a concrete directory, such as `!**/test-support/generated`. These patterns may start with `**/` and use `*` for an intermediate directory, as in `!**/test-support/*/generated`. Every other segment names a directory without dots except an optional leading dot; the final segment must be a name. File patterns, nested `**` and wildcard suffixes are rejected. Shipped preset declarations describe Quality's internal policy and are not subject to the consumer scope restriction.
82
+ Consumer exceptions belong in `quality.config.ts`'s `exceptions` list, as `{ files, rules, reason, subject? }`. A single entry covers Quality rules and individual `biome/lint/<group>/<rule>` or `biome/assist/<group>/<action>` rules on explicit file paths. Quality applies the native Biome settings at lint and fix time; do not mirror them in `biome.json`. See [exceptions and upgrades](./debt-and-hooks.md#exceptions).
83
+
84
+ Globs, whole checks and rule groups cannot be excepted. A `biome/files/includes` entry must retain `**` and add only negative patterns naming a concrete directory, such as `!**/test-support/generated`. These patterns may start with `**/` and use `*` for an intermediate directory, as in `!**/test-support/*/generated`. Every other segment names a directory without dots except an optional leading dot; the final segment must be a name. File patterns, nested `**` and wildcard suffixes are rejected. Shipped preset declarations describe Quality's internal policy and are not subject to the consumer scope restriction.
83
85
 
84
86
  Quality ships reasons for its own internal Biome exceptions. Extend the shared configuration and use the baseline for debt. `suppressions/biome-overrides` reads `biome.json` or `biome.jsonc` at the root, every nested `biome.json` and `biome.jsonc` among the checked files, and the configs each of them `extends`, resolved the way Biome resolves them (see [Shared presets](#shared-presets)). These settings count as overrides, at the top level and in every `overrides` entry:
85
87
 
@@ -99,30 +101,7 @@ Quality ships reasons for its own internal Biome exceptions. Extend the shared c
99
101
 
100
102
  Biome runs assist actions, key sorting among them, as part of `biome check` and reports what they would change, so turning one off is an override. Formatter options such as `indentStyle` or `lineWidth` choose a style, not an exception, and are not overrides. Settings that raise a rule or enable it are not overrides either. Rule options are not read, so an option that loosens a rule, such as a higher complexity limit, is left to review.
101
103
 
102
- ```ts
103
- export default defineConfig({
104
- rules: {
105
- "suppressions/biome-overrides": {
106
- options: {
107
- declared: [
108
- {
109
- rule: "lint/style/noDefaultExport",
110
- includes: ["scripts/tool.config.ts"],
111
- reason: "Tools load their config files through the default export.",
112
- },
113
- {
114
- rule: "lint/suspicious/noConsole",
115
- includes: ["scripts/report.ts"],
116
- reason: "Scripts report to the terminal they run in.",
117
- },
118
- ],
119
- },
120
- },
121
- },
122
- });
123
- ```
124
-
125
- A declaration covers a setting when its `rule` and its `includes` match the setting's exactly, in any order. An `includes` list that keeps files out is declared as the whole list, so every pattern added to it needs the declaration to change too. A setting at the top level of the root config has the scope `["**"]`, and an `overrides` entry has its own `includes` (`["**"]` when it has none). Paths in a nested config are relative to its folder, so a rule scoped to `"src/a.ts"` in `packages/web/biome.json` is declared as `"packages/web/src/a.ts"`. Top-level and globbed rule scopes cannot be declared by consumers. A setting without a declaration is reported at its line in the Biome config; a declaration that no setting matches is reported against the root config, so the list cannot outlive the overrides it explains.
104
+ A manual setting without a legacy declaration is reported at its line in the Biome config. The compatibility reader matches the old `{ rule, includes, reason }` declarations to settings with the same rule and exact scope, in any order, and reports stale or repeated declarations. Run `quality exceptions migrate` to replace both with the single config entry. A generated override never makes a checked-in manual override declared.
126
105
 
127
106
  #### Shared presets
128
107
 
@@ -134,7 +113,7 @@ A config `extends` its entries the way Biome does:
134
113
  - Lists are appended, not replaced: `overrides`, `plugins` and every `includes` list, `files.includes` among them. A repository's `files.includes` adds to the preset's, so each list is declared by whoever wrote it.
135
114
  - Patterns in an extended config resolve against the repository root, or against the folder of a nested config that extends it, as patterns in the config itself do.
136
115
 
137
- A package that ships a preset declares the weakenings the preset makes in a `declarations.json` beside the preset file, as an array of the same `{ rule, includes, reason }` entries the `declared` option takes. Its `includes` are written relative to the repository that extends the preset. Those weakenings count as declared, so the repository adds nothing for them:
116
+ A package that ships a preset declares the weakenings the preset makes in a `declarations.json` beside the preset file, as an array of `{ rule, includes, reason }` entries. Its `includes` are written relative to the repository that extends the preset. Those weakenings count as declared, so the repository adds nothing for them:
138
117
 
139
118
  ```json
140
119
  [
@@ -146,7 +125,7 @@ A package that ships a preset declares the weakenings the preset makes in a `dec
146
125
  ]
147
126
  ```
148
127
 
149
- A weakening the preset makes without such a declaration is reported at the line of the repository's `extends` entry, and the repository's own declaration covers it. A weakening the repository adds beyond the preset needs its own declaration, and a repository declaration that repeats one the preset ships is reported until it is removed. A preset entry that cannot be resolved or read, or whose `declarations.json` is invalid, is reported at the `extends` entry as well.
128
+ A weakening the preset makes without such a declaration is reported at the line of the repository's `extends` entry, and the repository must repair the preset or replace that setting with a config exception. A weakening the repository adds beyond the preset needs its own declaration, and a repository declaration that repeats one the preset ships is reported until it is removed. A preset entry that cannot be resolved or read, or whose `declarations.json` is invalid, is reported at the `extends` entry as well.
150
129
 
151
130
  #### Recommended Biome rules
152
131
 
@@ -181,7 +160,7 @@ export default defineConfig({
181
160
  });
182
161
  ```
183
162
 
184
- `imports/cycles` reports each group of modules that import each other at run time once, at the alphabetically first of them, with one loop through the group. Its count is the number of modules in the group. `import type`, `export type`, type references and imports in declaration files are left out; `import { type X }` stays a runtime import. Dynamic `import()` and `require()` count; `require.resolve()` does not. It takes no registry exceptions. Each finding of `imports/resolvable` has the import as its subject.
163
+ `imports/cycles` reports each group of modules that import each other at run time once, at the alphabetically first of them, with one loop through the group. Its count is the number of modules in the group. `import type`, `export type`, type references and imports in declaration files are left out; `import { type X }` stays a runtime import. Dynamic `import()` and `require()` count; `require.resolve()` does not. It takes no exceptions. Each finding of `imports/resolvable` has the import as its subject.
185
164
 
186
165
  When the sources hold no module at all, the graph-building rules stop the run instead of passing on an empty graph. Point `sources` at the code; do not disable the checks to conceal an empty inventory. `imports/aliased` does not require a graph, and `imports/fences` does nothing when no fences are declared.
187
166
 
@@ -254,13 +233,13 @@ The DSL does not compile without `demonstratedBy`, and the rule checks the examp
254
233
 
255
234
  Fences guard shipped code, and [test code](./naming.md#test-code) ships nowhere, so a fence never holds it: a test file or a file under `test-support/` may import across every fence. There is no option to check them.
256
235
 
257
- A finding has its fence's name as its subject, so a registry entry with that subject excuses one file from one fence.
236
+ A finding has its fence's name as its subject, so a config exception with that subject excuses one file from one fence.
258
237
 
259
- `imports/no-test-code` checks every file edge from a production module to a test file or a file under `test-support/`, including type imports, re-exports and test assets. It rejects missing relative paths under `test-support/` too. Test importers may still import production code and other test code. Move shared code into a production module and import it from both production and tests. The rule takes no registry exceptions.
238
+ `imports/no-test-code` checks every file edge from a production module to a test file or a file under `test-support/`, including type imports, re-exports and test assets. It rejects missing relative paths under `test-support/` too. Test importers may still import production code and other test code. Move shared code into a production module and import it from both production and tests. The rule takes no exceptions.
260
239
 
261
240
  ### Manifests
262
241
 
263
- `manifests/sorted` keeps every `package.json` in the repository in the key order of [sort-package-json](https://github.com/keithamus/sort-package-json), a dependency of this package. It walks the whole repository, not only the sources, and skips untracked files ignored by git and `node_modules`. A manifest that is not valid JSON is reported too. `quality fix` sorts the manifests, and the rule takes no registry exceptions.
242
+ `manifests/sorted` keeps every `package.json` in the repository in the key order of [sort-package-json](https://github.com/keithamus/sort-package-json), a dependency of this package. It walks the whole repository, not only the sources, and skips untracked files ignored by git and `node_modules`. A manifest that is not valid JSON is reported too. `quality fix` sorts the manifests, and the rule takes no exceptions.
264
243
 
265
244
  ## Local-rule API
266
245
 
@@ -307,4 +286,4 @@ export const limited = defineRule({
307
286
  });
308
287
  ```
309
288
 
310
- This is the option-bearing rule the package tests. It defaults to two files and validates a configured `max` before the asynchronous check runs. A rule without an options schema refuses options. Set `registrable: false` when a rule must never be excused by a registry entry.
289
+ This is the option-bearing rule the package tests. It defaults to two files and validates a configured `max` before the asynchronous check runs. A rule without an options schema refuses options. Set `registrable: false` when a rule must never be excused by a config exception.
package/docs/tooling.md CHANGED
@@ -26,7 +26,7 @@ The preset turns off `noUnusedVariables` and `noUnusedFunctionParameters`, becau
26
26
 
27
27
  The preset reports `useSortedClasses` at `error` with its fix enabled for class attributes and calls to `cn`, `clsx` and `cva`. Tailwind output does not depend on the order of classes in an attribute, so sorting is safe.
28
28
 
29
- The preset sets `noDelete` to `off` because its fix changed behavior or did not finish on real code. `noUndeclaredClasses` is off because its class resolution does not cover aliases and Tailwind utilities; `noInlineStyles` is off because computed values need inline styles. Quality owns these choices and ships their reasons in `declarations.json`. They do not require consumer declarations. The exposed consumer-declaration API is described under [declared Biome overrides](./rules.md#declared-biome-overrides); use the shared defaults for adoption.
29
+ The preset sets `noDelete` to `off` because its fix changed behavior or did not finish on real code. `noUndeclaredClasses` is off because its class resolution does not cover aliases and Tailwind utilities; `noInlineStyles` is off because computed values need inline styles. Quality owns these choices and ships their reasons in `declarations.json`. They do not require consumer declarations. The config exception API is described under [declared Biome overrides](./rules.md#declared-biome-overrides); use the shared defaults for adoption.
30
30
 
31
31
  The preset keeps the following rules at `error` with their fixes off so the author decides the repair: `noAccessKey`, `noAriaHiddenOnFocusable`, `noAutofocus`, `noInteractiveElementToNoninteractiveRole`, `noNoninteractiveElementToInteractiveRole`, `noNoninteractiveTabindex`, `noRedundantRoles`, `useValidAriaProps`, `useValidAriaRole`, `noImportantStyles`, `noConstAssign`, `noProcessGlobal`, `useJsonImportAttributes`, `noUnusedPrivateClassMembers`, `useExhaustiveDependencies`, `noFloatingPromises`, `noMisusedPromises`, `useExhaustiveSwitchCases`, `noEqualsToNull`, `useConsistentArrayType`, `useConsistentCurlyBraces`, `useConsistentTestIt`, `useRegexpTest`, `useUnicodeRegex`, `noNonNullAssertion`, `useAtIndex`, `noParametersOnlyUsedInRecursion`, `noSkippedTests` and `useNamingConvention`. The fix command also skips `noDuplicateObjectKeys` and `useSortedKeys`. Import `process` from `node:process` in Node code or use the application's Effect service. Write JSON imports with `with { type: "json" }` for Node 24; the fixer leaves both choices to the author.
32
32
 
@@ -38,7 +38,7 @@ The preset keeps the following rules at `error` with their fixes off so the auth
38
38
 
39
39
  `noSkippedTests` has no automatic fix because enabling a skipped test can change which setup and bodies run; the author must repair the test or remove a claim it cannot prove.
40
40
 
41
- The `biome` rule runs `biome check` with the repository's config and reports each finding as `biome/<category>`, such as `biome/lint/style/useBlockStatements`, `biome/assist/source/useSortedKeys`, `biome/format` or `biome/plugin`, so Biome's findings go through the baseline like any other rule's. `quality baseline write --rule biome` takes in every Biome category at once. A finding below `error` is not reported; a consumer exception for an individual rule therefore needs an explicit file scope and a reason. Whole-check, group and blanket-scope declarations are rejected; see [configuration limits](../README.md#configuration-limits). The rule also asks for a root `biome.json` or `biome.jsonc` that extends the preset, and it takes no registry exceptions. A Biome config that Biome cannot load stops the run with Biome's message.
41
+ The `biome` rule runs `biome check` with the repository's config and reports each finding as `biome/<category>`, such as `biome/lint/style/useBlockStatements`, `biome/assist/source/useSortedKeys`, `biome/format` or `biome/plugin`, so Biome's findings go through the baseline like any other rule's. `quality baseline write --rule biome` takes in every Biome category at once. A finding below `error` is not reported; a consumer exception for an individual rule therefore lives in the config exceptions list with explicit files and a reason. Quality applies its Biome settings during lint and fix. Whole-check, group and blanket-scope declarations are rejected; see [configuration limits](../README.md#configuration-limits). The rule also asks for a root `biome.json` or `biome.jsonc` that extends the preset, and it refuses legacy registry exceptions. A Biome config that Biome cannot load stops the run with Biome's message.
42
42
 
43
43
  `quality fix` sorts the discovered manifests, then runs `biome check --write --unsafe`, with the two skipped fixes above. It then runs Biome's formatter, because a lint fix can leave code unformatted. One fix can make room for another, so it repeats both passes until a round rewrites nothing, at most five rounds. When files still change in the fifth round, it names the changed text files it can discover and exits 2; when those files are outside its inventory, the error gives a verbose Biome command to name them. An unsafe fix can change behavior, such as `==` becoming `===`, so review what it changed.
44
44
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shivaedev/quality",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "description": "Repository quality gate with typed rules, a baseline of existing debt and a registry of reasoned exceptions",
5
5
  "homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/quality#readme",
6
6
  "bugs": {
@@ -0,0 +1,70 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { access, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { Schema } from "effect";
5
+ import ts from "typescript";
6
+ import type { ExceptionDeclaration } from "#exceptions/declaration.ts";
7
+
8
+ function nativeOverride(files: readonly string[], rule: string): unknown {
9
+ const [, tool, group = "", name = ""] = rule.split("/");
10
+ const key = tool === "lint" ? "linter" : "assist";
11
+ const settings = tool === "lint" ? "rules" : "actions";
12
+ return { includes: files, [key]: { [settings]: { [group]: { [name]: "off" } } } };
13
+ }
14
+
15
+ export function exceptionConfig(declarations: readonly ExceptionDeclaration[]): {
16
+ readonly includes: readonly string[];
17
+ readonly overrides: readonly unknown[];
18
+ } {
19
+ const includes = declarations.filter((entry) => entry.rules.includes("biome/files/includes")).flatMap((entry) => entry.files);
20
+ return {
21
+ includes: includes.length === 0 ? [] : ["**", ...includes.filter((pattern) => pattern !== "**")],
22
+ overrides: declarations.flatMap(({ files, rules }) =>
23
+ rules.filter((rule) => /^biome\/(?:lint|assist)\//u.test(rule)).map((rule) => nativeOverride(files, rule)),
24
+ ),
25
+ };
26
+ }
27
+
28
+ export async function withExceptionConfig<TValue>(
29
+ root: string,
30
+ declarations: readonly ExceptionDeclaration[],
31
+ run: (args: readonly string[]) => Promise<TValue>,
32
+ ): Promise<TValue> {
33
+ const { includes, overrides } = exceptionConfig(declarations);
34
+ if (includes.length === 0 && overrides.length === 0) {
35
+ return run([]);
36
+ }
37
+ const name = `.quality-biome-${randomUUID()}.json`;
38
+ const path = join(root, name);
39
+ const config = await access(join(root, "biome.json")).then(
40
+ () => "biome.json",
41
+ () => "biome.jsonc",
42
+ );
43
+ const text = await readFile(join(root, config), "utf8");
44
+ const parsed = ts.parseConfigFileTextToJson(config, text);
45
+ if (parsed.error !== undefined) {
46
+ throw new Error(`Cannot load ${config}: ${ts.flattenDiagnosticMessageText(parsed.error.messageText, " ")}`);
47
+ }
48
+ const own = Schema.decodeUnknownSync(Schema.Record(Schema.String, Schema.Unknown))(parsed.config);
49
+ const files = Schema.is(Schema.Record(Schema.String, Schema.Unknown))(own.files) ? own.files : {};
50
+ if (
51
+ (own.files !== undefined && !Schema.is(Schema.Record(Schema.String, Schema.Unknown))(own.files))
52
+ || (files.includes !== undefined && !Array.isArray(files.includes))
53
+ || (own.overrides !== undefined && !Array.isArray(own.overrides))
54
+ ) {
55
+ throw new Error(`Cannot merge exceptions into invalid ${config}: files, includes or overrides has the wrong shape.`);
56
+ }
57
+ const previousIncludes = Array.isArray(files.includes) ? files.includes : ["**"];
58
+ const previousOverrides = Array.isArray(own.overrides) ? own.overrides : [];
59
+ const generated = {
60
+ ...own,
61
+ files: { ...files, includes: [...previousIncludes, ...includes.filter((pattern) => pattern !== "**"), `!${name}`] },
62
+ overrides: [...previousOverrides, ...overrides],
63
+ };
64
+ try {
65
+ await writeFile(path, JSON.stringify(generated), { flag: "wx" });
66
+ return await run([`--config-path=${path}`]);
67
+ } finally {
68
+ await rm(path, { force: true });
69
+ }
70
+ }
@@ -1,5 +1,6 @@
1
1
  import { relative, sep } from "node:path";
2
2
  import { Schema } from "effect";
3
+ import type { ExceptionDeclaration } from "#exceptions/declaration.ts";
3
4
  import { ignoredTrackedPaths } from "#inventory/trackedPaths.ts";
4
5
  import { recognizeDiagnostics } from "./recognizeDiagnostics.ts";
5
6
  import { type BiomeRun, runBiome } from "./run.ts";
@@ -28,9 +29,14 @@ function failure(args: readonly string[], run: BiomeRun): Error {
28
29
  return new Error(`biome ${args.join(" ")} exited ${run.code} without a report:\n${[run.stdout, run.stderr].join("\n").trim()}`);
29
30
  }
30
31
 
31
- async function reportFor(root: string, args: readonly string[], paths: readonly string[]): Promise<Report> {
32
+ async function reportFor(
33
+ root: string,
34
+ args: readonly string[],
35
+ paths: readonly string[],
36
+ exceptions: readonly ExceptionDeclaration[],
37
+ ): Promise<Report> {
32
38
  const command = [...args, "--reporter=json", "--max-diagnostics=none", ...paths];
33
- const run = await runBiome(root, command);
39
+ const run = await runBiome(root, command, exceptions);
34
40
  const report = decodeReport(run.stdout.trim());
35
41
  if (report._tag === "None") {
36
42
  throw failure(command, run);
@@ -38,12 +44,12 @@ async function reportFor(root: string, args: readonly string[], paths: readonly
38
44
  return report.value;
39
45
  }
40
46
 
41
- export async function biomeReport(root: string, args: readonly string[]): Promise<Report> {
42
- const [report, tracked] = await Promise.all([reportFor(root, args, ["."]), ignoredTrackedPaths(root)]);
47
+ export async function biomeReport(root: string, args: readonly string[], exceptions: readonly ExceptionDeclaration[] = []): Promise<Report> {
48
+ const [report, tracked] = await Promise.all([reportFor(root, args, ["."], exceptions), ignoredTrackedPaths(root)]);
43
49
  const paths = tracked
44
50
  .map((path) => relative(root, path))
45
51
  .filter((path) => !path.split(sep).some((part) => part === ".git" || part === "node_modules"));
46
- const extra = paths.length === 0 ? undefined : await reportFor(root, [...args, "--vcs-enabled=false"], paths);
52
+ const extra = paths.length === 0 ? undefined : await reportFor(root, [...args, "--vcs-enabled=false"], paths, exceptions);
47
53
  const diagnostics = [...report.diagnostics, ...(extra?.diagnostics ?? [])];
48
54
  const unique = [...new Map(diagnostics.map((diagnostic) => [JSON.stringify(diagnostic), diagnostic])).values()];
49
55
  return {
package/src/biome/run.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { createRequire } from "node:module";
3
3
  import process from "node:process";
4
+ import type { ExceptionDeclaration } from "#exceptions/declaration.ts";
5
+ import { withExceptionConfig } from "./exceptionConfig.ts";
4
6
 
5
7
  export interface BiomeRun {
6
8
  readonly code: number;
@@ -10,14 +12,19 @@ export interface BiomeRun {
10
12
 
11
13
  const BIN = createRequire(import.meta.url).resolve("@biomejs/biome/bin/biome");
12
14
 
13
- export function runBiome(root: string, args: readonly string[]): Promise<BiomeRun> {
14
- return new Promise((resolve, reject) => {
15
- execFile(process.execPath, [BIN, ...args, "--colors=off"], { cwd: root, maxBuffer: 2 ** 30 }, (error, stdout, stderr) => {
16
- if (error !== null && typeof error.code !== "number") {
17
- reject(error);
18
- return;
19
- }
20
- resolve({ code: error === null ? 0 : Number(error.code), stderr, stdout });
21
- });
22
- });
15
+ export function runBiome(root: string, args: readonly string[], exceptions: readonly ExceptionDeclaration[] = []): Promise<BiomeRun> {
16
+ return withExceptionConfig(
17
+ root,
18
+ exceptions,
19
+ (configArgs) =>
20
+ new Promise((resolve, reject) => {
21
+ execFile(process.execPath, [BIN, ...args, ...configArgs, "--colors=off"], { cwd: root, maxBuffer: 2 ** 30 }, (error, stdout, stderr) => {
22
+ if (error !== null && typeof error.code !== "number") {
23
+ reject(error);
24
+ return;
25
+ }
26
+ resolve({ code: error === null ? 0 : Number(error.code), stderr, stdout });
27
+ });
28
+ }),
29
+ );
23
30
  }
package/src/cli/args.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  import { parseArgs } from "node:util";
2
2
  import { Result } from "effect";
3
3
  import type { WarningDetail } from "#report/render.ts";
4
+ import { ADOPT_REMOVED, USAGE as COMMAND_USAGE } from "./messages.ts";
4
5
 
5
6
  export type Command =
6
7
  | { readonly _tag: "Lint"; readonly config: string | undefined; readonly warnings: WarningDetail }
8
+ | { readonly _tag: "ExceptionsMigrate"; readonly config: string | undefined; readonly from: string | undefined }
7
9
  | { readonly _tag: "Fix"; readonly config: string | undefined }
8
10
  | { readonly _tag: "BaselineWrite"; readonly config: string | undefined; readonly rules: readonly string[] }
9
11
  | { readonly _tag: "BaselinePrune"; readonly config: string | undefined; readonly against: string | undefined }
@@ -16,32 +18,7 @@ export type Command =
16
18
 
17
19
  export type Parsed = { readonly _tag: "Parsed"; readonly command: Command } | { readonly _tag: "Usage"; readonly problem: string };
18
20
 
19
- export const USAGE = `Usage:
20
- quality lint [--config <file>] [--warnings summary|all]
21
- quality fix [--config <file>]
22
- quality baseline write [--config <file>] [--rule <id>]...
23
- quality baseline prune [--config <file>] [--against <ref>]
24
- quality baseline tighten [--config <file>] [--staged]
25
- quality baseline migrate [--config <file>] [--from <file>]
26
- quality hooks install [--config <file>] [--force]
27
- quality hooks uninstall
28
- quality hooks pre-commit [--config <file>]
29
-
30
- lint Run every rule. Exits 1 on an error-level violation, a file over its baseline, a stale registry entry
31
- or a baseline entry for a rule that is off or unknown.
32
- fix Sort every package.json, apply Biome's lint fixes, unsafe ones included, its assist actions and formatting, then format again.
33
- baseline write Record current error-level violations. Creates the baseline, or records the named rules again, replacing their entries.
34
- baseline prune Drop fixed debt, lower entries to what is left and carry entries to files git saw move. Never adds or raises an entry.
35
- baseline tighten Prune only the entries of files changed since HEAD, or with --staged, in the index.
36
- baseline migrate Move a baseline from the earlier JSON format (--from, quality/baseline.json by default) to the configured file.
37
- hooks install Install the git pre-commit hook, shared by every worktree, that runs hooks pre-commit. Keeps a hook that is not quality's
38
- unless --force replaces it.
39
- hooks uninstall Remove quality's pre-commit hook. Keeps a hook that is not quality's.
40
- hooks pre-commit What the hook runs: lint, then package.json scripts typecheck and quality:commit when present.
41
-
42
- --config <file> Config file; its directory is the repository root. Without this option, uses ./quality.config.ts when present, otherwise defaults.
43
- --against <ref> The branch the work merges into, whose merge base prune follows moves from. Defaults to origin/HEAD, then origin/main, then origin/master.
44
- Exit codes: 0 passed, 1 failed the gate, 2 could not run.`;
21
+ export const USAGE = COMMAND_USAGE;
45
22
 
46
23
  const OPTIONS = {
47
24
  against: { type: "string" },
@@ -56,11 +33,9 @@ const OPTIONS = {
56
33
 
57
34
  const usage = (problem: string): Parsed => ({ _tag: "Usage", problem });
58
35
 
59
- const ADOPT_REMOVED =
60
- "`quality adopt` was removed in 0.7.0. To record a rule's existing findings in the baseline, run `quality baseline write --rule <id>`; to apply the fixes Biome can make first, run `quality fix`.";
61
-
62
36
  const SUBCOMMANDS: Readonly<Record<string, string>> = {
63
37
  baseline: "baseline takes write, prune, tighten or migrate.",
38
+ exceptions: "exceptions takes migrate.",
64
39
  hooks: "hooks takes install, uninstall or pre-commit.",
65
40
  };
66
41
 
@@ -77,6 +52,7 @@ const ACCEPTS: Readonly<Record<string, readonly Option[]>> = {
77
52
  "baseline prune": ["against"],
78
53
  "baseline tighten": ["staged"],
79
54
  "baseline write": ["rule"],
55
+ "exceptions migrate": ["from"],
80
56
  fix: [],
81
57
  "hooks install": ["force"],
82
58
  "hooks pre-commit": [],
@@ -102,6 +78,8 @@ const commandFor = (name: string, values: Values): Parsed => {
102
78
  ? parsed({ _tag: "Lint", config, warnings })
103
79
  : usage(`--warnings takes summary or all, not "${warnings}".`);
104
80
  }
81
+ case "exceptions migrate":
82
+ return parsed({ _tag: "ExceptionsMigrate", config, from: values.from });
105
83
  case "fix":
106
84
  return parsed({ _tag: "Fix", config });
107
85
  case "baseline write":
package/src/cli/fix.ts CHANGED
@@ -2,6 +2,7 @@ import { relative } from "node:path";
2
2
  import { Console, Effect, FileSystem } from "effect";
3
3
  import { biomeReport, type Report } from "#biome/report.ts";
4
4
  import { loadConfig } from "#config/load.ts";
5
+ import type { ExceptionDeclaration } from "#exceptions/declaration.ts";
5
6
  import { SetupFailure } from "#failure.ts";
6
7
  import { posix } from "#inventory/ignore-scope.ts";
7
8
  import { walk } from "#inventory/walk.ts";
@@ -10,10 +11,10 @@ import { plural } from "#report/plural.ts";
10
11
 
11
12
  export const MAX_FIX_ROUNDS = 5;
12
13
 
13
- function biome(root: string, args: readonly string[]): Effect.Effect<Report, SetupFailure> {
14
+ function biome(root: string, args: readonly string[], exceptions: readonly ExceptionDeclaration[]): Effect.Effect<Report, SetupFailure> {
14
15
  return Effect.tryPromise({
15
16
  catch: (cause) => new SetupFailure({ message: cause instanceof Error ? cause.message : String(cause) }),
16
- try: () => biomeReport(root, args),
17
+ try: () => biomeReport(root, args, exceptions),
17
18
  });
18
19
  }
19
20
 
@@ -43,17 +44,21 @@ export interface Settling<TFailure, TRequirements> {
43
44
  readonly snapshot: Effect.Effect<ReadonlyMap<string, string>, TFailure, TRequirements>;
44
45
  }
45
46
 
46
- function changedBy(root: string, args: readonly string[]): Effect.Effect<number, SetupFailure> {
47
- return Effect.map(biome(root, args), (report) => report.summary.changed);
47
+ function changedBy(root: string, args: readonly string[], exceptions: readonly ExceptionDeclaration[]): Effect.Effect<number, SetupFailure> {
48
+ return Effect.map(biome(root, args, exceptions), (report) => report.summary.changed);
48
49
  }
49
50
 
50
- function biomePasses(root: string): readonly Pass<SetupFailure, never>[] {
51
+ function biomePasses(root: string, exceptions: readonly ExceptionDeclaration[]): readonly Pass<SetupFailure, never>[] {
51
52
  return [
52
53
  {
53
54
  label: "with fixes",
54
- run: changedBy(root, ["check", "--write", "--unsafe", "--skip=assist/source/useSortedKeys", "--skip=lint/suspicious/noDuplicateObjectKeys"]),
55
+ run: changedBy(
56
+ root,
57
+ ["check", "--write", "--unsafe", "--skip=assist/source/useSortedKeys", "--skip=lint/suspicious/noDuplicateObjectKeys"],
58
+ exceptions,
59
+ ),
55
60
  },
56
- { label: "with the format pass", run: changedBy(root, ["format", "--write"]) },
61
+ { label: "with the format pass", run: changedBy(root, ["format", "--write"], exceptions) },
57
62
  ];
58
63
  }
59
64
 
@@ -122,9 +127,9 @@ export function settle<TFailure, TRequirements>({
122
127
 
123
128
  export function fix(cwd: string, config: string | undefined): Effect.Effect<void, SetupFailure, FileSystem.FileSystem> {
124
129
  return Effect.gen(function* () {
125
- const { root } = yield* loadConfig(cwd, config);
130
+ const { root, exceptions } = yield* loadConfig(cwd, config);
126
131
  const sorted = yield* Effect.mapError(sortManifests(root), (failure) => new SetupFailure({ message: failure.message }));
127
132
  yield* Console.log(`quality: sort-package-json rewrote ${plural(sorted, "manifest")}.`);
128
- yield* settle({ passes: biomePasses(root), snapshot: snapshotOf(root) });
133
+ yield* settle({ passes: biomePasses(root, exceptions), snapshot: snapshotOf(root) });
129
134
  });
130
135
  }
package/src/cli/lint.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { relative } from "node:path";
1
2
  import { Console, Effect, type FileSystem } from "effect";
2
3
  import { evaluate, passes } from "#engine/evaluate.ts";
3
4
  import { openSession } from "#engine/session.ts";
@@ -17,7 +18,8 @@ export const lint = (
17
18
  baseline: session.config.baseline,
18
19
  checked: session.inventory.sources.length,
19
20
  descriptions: new Map(session.config.active.map((rule) => [rule.id, rule.description])),
20
- registry: session.config.registry,
21
+ registry:
22
+ session.config.exceptions.length === 0 ? session.config.registry : `${relative(session.config.root, session.config.file)} exceptions`,
21
23
  warnings,
22
24
  }),
23
25
  );
@@ -0,0 +1,31 @@
1
+ export const USAGE = `Usage:
2
+ quality lint [--config <file>] [--warnings summary|all]
3
+ quality fix [--config <file>]
4
+ quality exceptions migrate [--config <file>] [--from <registry>]
5
+ quality baseline write [--config <file>] [--rule <id>]...
6
+ quality baseline prune [--config <file>] [--against <ref>]
7
+ quality baseline tighten [--config <file>] [--staged]
8
+ quality baseline migrate [--config <file>] [--from <file>]
9
+ quality hooks install [--config <file>] [--force]
10
+ quality hooks uninstall
11
+ quality hooks pre-commit [--config <file>]
12
+
13
+ lint Run every rule. Exits 1 on an error-level violation, a file over its baseline, a stale registry entry
14
+ or a baseline entry for a rule that is off or unknown.
15
+ fix Sort every package.json, apply Biome's lint fixes, unsafe ones included, its assist actions and formatting, then format again.
16
+ exceptions migrate Move legacy registry entries and Biome declarations into quality.config.ts exceptions and remove matching root overrides.
17
+ baseline write Record current error-level violations. Creates the baseline, or records the named rules again, replacing their entries.
18
+ baseline prune Drop fixed debt, lower entries to what is left and carry entries to files git saw move. Never adds or raises an entry.
19
+ baseline tighten Prune only the entries of files changed since HEAD, or with --staged, in the index.
20
+ baseline migrate Move a baseline from the earlier JSON format (--from, quality/baseline.json by default) to the configured file.
21
+ hooks install Install the git pre-commit hook, shared by every worktree, that runs hooks pre-commit. Keeps a hook that is not quality's
22
+ unless --force replaces it.
23
+ hooks uninstall Remove quality's pre-commit hook. Keeps a hook that is not quality's.
24
+ hooks pre-commit What the hook runs: lint, then package.json scripts typecheck and quality:commit when present.
25
+
26
+ --config <file> Config file; its directory is the repository root. Without this option, uses ./quality.config.ts when present, otherwise defaults.
27
+ --against <ref> The branch the work merges into, whose merge base prune follows moves from. Defaults to origin/HEAD, then origin/main, then origin/master.
28
+ Exit codes: 0 passed, 1 failed the gate, 2 could not run.`;
29
+
30
+ export const ADOPT_REMOVED =
31
+ "`quality adopt` was removed in 0.7.0. To record a rule's existing findings in the baseline, run `quality baseline write --rule <id>`; to apply the fixes Biome can make first, run `quality fix`.";
@@ -0,0 +1,87 @@
1
+ import { join } from "node:path";
2
+ import { Console, Effect, FileSystem } from "effect";
3
+ import { loadConfig } from "#config/load.ts";
4
+ import { validOrFail } from "#decoded.ts";
5
+ import { readInput } from "#engine/baseline-file.ts";
6
+ import { decodeExceptions, type ExceptionDeclaration } from "#exceptions/declaration.ts";
7
+ import { migrateBiome } from "#exceptions/migrateBiome.ts";
8
+ import { migrateConfig } from "#exceptions/migrateConfig.ts";
9
+ import { decodeRegistry } from "#exceptions/registry.ts";
10
+ import { SetupFailure } from "#failure.ts";
11
+ import { writeText } from "#inventory/filesystem.ts";
12
+
13
+ function merge(entries: readonly ExceptionDeclaration[]): readonly ExceptionDeclaration[] {
14
+ const merged: ExceptionDeclaration[] = [];
15
+ for (const entry of entries) {
16
+ const index = merged.findIndex(
17
+ (other) =>
18
+ other.reason === entry.reason
19
+ && other.subject === entry.subject
20
+ && other.files.length === entry.files.length
21
+ && other.files.every((file) => entry.files.includes(file)),
22
+ );
23
+ const previous = merged[index];
24
+ if (previous === undefined) {
25
+ merged.push(entry);
26
+ } else {
27
+ merged[index] = {
28
+ ...previous,
29
+ rules: [previous.rules[0], ...new Set([...previous.rules.slice(1), ...entry.rules.filter((rule) => rule !== previous.rules[0])])],
30
+ };
31
+ }
32
+ }
33
+ return merged;
34
+ }
35
+
36
+ export const migrateExceptions = Effect.fnUntraced(function* (
37
+ cwd: string,
38
+ configPath: string | undefined,
39
+ from: string | undefined,
40
+ ): Effect.fn.Return<void, SetupFailure, FileSystem.FileSystem> {
41
+ const config = yield* loadConfig(cwd, configPath);
42
+ const registryPath = from ?? config.registry;
43
+ const raw = yield* readInput(config.root, registryPath);
44
+ if (from !== undefined && raw === undefined) {
45
+ return yield* new SetupFailure({ message: `${registryPath} does not exist. Pass an existing registry file with --from.` });
46
+ }
47
+ const registry = yield* validOrFail(registryPath, yield* Effect.promise(() => decodeRegistry(raw)));
48
+ const entries = merge([
49
+ ...config.exceptions,
50
+ ...registry.map(
51
+ ({ file, reason, rule, subject }): ExceptionDeclaration => ({
52
+ files: [file],
53
+ reason,
54
+ rules: [rule],
55
+ ...(subject === undefined ? {} : { subject }),
56
+ }),
57
+ ),
58
+ ...config.biomeDeclarations.map(({ includes, reason, rule }): ExceptionDeclaration => ({ files: includes, reason, rules: [`biome/${rule}`] })),
59
+ ]);
60
+ yield* validOrFail(config.file, yield* Effect.promise(() => decodeExceptions(entries)));
61
+ const text = yield* readInput("", config.file);
62
+ const next = yield* Effect.try({
63
+ catch: (cause) => new SetupFailure({ message: String(cause) }),
64
+ try: () => migrateConfig(text, config.file, entries, config.biomeDeclarations.length > 0),
65
+ });
66
+ const updates: Array<{ readonly path: string; readonly text: string }> = [{ path: config.file, text: next }];
67
+ if (config.biomeDeclarations.length > 0) {
68
+ for (const file of ["biome.json", "biome.jsonc"]) {
69
+ const biome = yield* readInput(config.root, file);
70
+ if (biome !== undefined) {
71
+ const updated = yield* Effect.try({
72
+ catch: (cause) => new SetupFailure({ message: String(cause) }),
73
+ try: () => migrateBiome(biome, file, config.biomeDeclarations),
74
+ });
75
+ updates.push({ path: join(config.root, file), text: updated });
76
+ }
77
+ }
78
+ }
79
+ for (const update of updates) {
80
+ yield* writeText(update.path, update.text).pipe(Effect.mapError((failure) => new SetupFailure({ message: failure.message })));
81
+ }
82
+ if (raw !== undefined) {
83
+ const fs = yield* FileSystem.FileSystem;
84
+ yield* fs.remove(join(config.root, registryPath)).pipe(Effect.mapError((failure) => new SetupFailure({ message: failure.message })));
85
+ }
86
+ yield* Console.log(`quality: moved exceptions to ${config.file}. Run quality fix and review the changes; remove unused declaration imports.`);
87
+ });
package/src/cli.ts CHANGED
@@ -6,6 +6,7 @@ import { writeBaseline } from "#cli/baseline.ts";
6
6
  import { fix } from "#cli/fix.ts";
7
7
  import { lint } from "#cli/lint.ts";
8
8
  import { migrateBaseline } from "#cli/migrate.ts";
9
+ import { migrateExceptions } from "#cli/migrateExceptions.ts";
9
10
  import { runMain } from "#cli/run-main.ts";
10
11
  import { pruneBaseline, tightenBaseline } from "#cli/shrink.ts";
11
12
  import { installHook } from "#hooks/install.ts";
@@ -33,6 +34,8 @@ const program = Effect.gen(function* () {
33
34
  return yield* pruneBaseline(process.cwd(), command.config, command.against);
34
35
  case "BaselineTighten":
35
36
  return yield* tightenBaseline(process.cwd(), command.config, command.staged);
37
+ case "ExceptionsMigrate":
38
+ return yield* migrateExceptions(process.cwd(), command.config, command.from);
36
39
  case "BaselineMigrate":
37
40
  return yield* migrateBaseline(process.cwd(), command.config, command.from);
38
41
  case "HooksInstall":
@@ -1,5 +1,6 @@
1
1
  import { Schema } from "effect";
2
2
  import { type Decoded, decodeWith } from "#decoded.ts";
3
+ import { ExceptionDeclaration } from "#exceptions/declaration.ts";
3
4
  import type { Rule } from "#rule.ts";
4
5
 
5
6
  const isRule = (value: unknown): value is Rule =>
@@ -30,6 +31,7 @@ const PreCommitRemoved = Schema.Never.annotate({
30
31
  const ConfigInput = Schema.Struct({
31
32
  adopt: Schema.optionalKey(AdoptRemoved),
32
33
  baseline: Schema.optionalKey(Schema.String),
34
+ exceptions: Schema.optionalKey(Schema.Array(ExceptionDeclaration)),
33
35
  extensions: Schema.optionalKey(Schema.Array(Schema.String)),
34
36
  local: Schema.optionalKey(Schema.Array(LocalRule)),
35
37
  preCommit: Schema.optionalKey(PreCommitRemoved),