@shivaedev/quality 0.21.1 → 0.23.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 (143) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +24 -8
  3. package/dist/biome/exceptionConfig.d.ts +7 -0
  4. package/dist/biome/exceptionConfig.d.ts.map +1 -0
  5. package/dist/biome/exceptionConfig.js +54 -0
  6. package/dist/biome/exceptionConfig.js.map +1 -0
  7. package/dist/biome/report.d.ts +2 -1
  8. package/dist/biome/report.d.ts.map +1 -1
  9. package/dist/biome/report.js +5 -5
  10. package/dist/biome/report.js.map +1 -1
  11. package/dist/biome/run.d.ts +2 -1
  12. package/dist/biome/run.d.ts.map +1 -1
  13. package/dist/biome/run.js +5 -4
  14. package/dist/biome/run.js.map +1 -1
  15. package/dist/cli/args.d.ts +5 -1
  16. package/dist/cli/args.d.ts.map +1 -1
  17. package/dist/cli/args.js +6 -27
  18. package/dist/cli/args.js.map +1 -1
  19. package/dist/cli/fix.d.ts.map +1 -1
  20. package/dist/cli/fix.js +9 -9
  21. package/dist/cli/fix.js.map +1 -1
  22. package/dist/cli/lint.d.ts.map +1 -1
  23. package/dist/cli/lint.js +2 -1
  24. package/dist/cli/lint.js.map +1 -1
  25. package/dist/cli/messages.d.ts +3 -0
  26. package/dist/cli/messages.d.ts.map +1 -0
  27. package/dist/cli/messages.js +30 -0
  28. package/dist/cli/messages.js.map +1 -0
  29. package/dist/cli/migrateExceptions.d.ts +4 -0
  30. package/dist/cli/migrateExceptions.d.ts.map +1 -0
  31. package/dist/cli/migrateExceptions.js +78 -0
  32. package/dist/cli/migrateExceptions.js.map +1 -0
  33. package/dist/cli/program.d.ts +9 -0
  34. package/dist/cli/program.d.ts.map +1 -0
  35. package/dist/cli/program.js +44 -0
  36. package/dist/cli/program.js.map +1 -0
  37. package/dist/cli/run-main.d.ts +5 -2
  38. package/dist/cli/run-main.d.ts.map +1 -1
  39. package/dist/cli/run-main.js +14 -10
  40. package/dist/cli/run-main.js.map +1 -1
  41. package/dist/cli.js +2 -46
  42. package/dist/cli.js.map +1 -1
  43. package/dist/config/decode.d.ts +6 -0
  44. package/dist/config/decode.d.ts.map +1 -1
  45. package/dist/config/decode.js +2 -0
  46. package/dist/config/decode.js.map +1 -1
  47. package/dist/config/load.d.ts +4 -0
  48. package/dist/config/load.d.ts.map +1 -1
  49. package/dist/config/load.js +10 -1
  50. package/dist/config/load.js.map +1 -1
  51. package/dist/config/resolve.d.ts.map +1 -1
  52. package/dist/config/resolve.js +11 -1
  53. package/dist/config/resolve.js.map +1 -1
  54. package/dist/config.d.ts +2 -0
  55. package/dist/config.d.ts.map +1 -1
  56. package/dist/config.js.map +1 -1
  57. package/dist/engine/run-rules.d.ts +1 -0
  58. package/dist/engine/run-rules.d.ts.map +1 -1
  59. package/dist/engine/run-rules.js +19 -4
  60. package/dist/engine/run-rules.js.map +1 -1
  61. package/dist/engine/session.d.ts.map +1 -1
  62. package/dist/engine/session.js +11 -3
  63. package/dist/engine/session.js.map +1 -1
  64. package/dist/exceptions/biomeFindings.d.ts +4 -0
  65. package/dist/exceptions/biomeFindings.d.ts.map +1 -0
  66. package/dist/exceptions/biomeFindings.js +14 -0
  67. package/dist/exceptions/biomeFindings.js.map +1 -0
  68. package/dist/exceptions/declaration.d.ts +11 -0
  69. package/dist/exceptions/declaration.d.ts.map +1 -0
  70. package/dist/exceptions/declaration.js +24 -0
  71. package/dist/exceptions/declaration.js.map +1 -0
  72. package/dist/exceptions/migrateBiome.d.ts +3 -0
  73. package/dist/exceptions/migrateBiome.d.ts.map +1 -0
  74. package/dist/exceptions/migrateBiome.js +62 -0
  75. package/dist/exceptions/migrateBiome.js.map +1 -0
  76. package/dist/exceptions/migrateConfig.d.ts +3 -0
  77. package/dist/exceptions/migrateConfig.d.ts.map +1 -0
  78. package/dist/exceptions/migrateConfig.js +57 -0
  79. package/dist/exceptions/migrateConfig.js.map +1 -0
  80. package/dist/exceptions/registryEntries.d.ts +4 -0
  81. package/dist/exceptions/registryEntries.d.ts.map +1 -0
  82. package/dist/exceptions/registryEntries.js +6 -0
  83. package/dist/exceptions/registryEntries.js.map +1 -0
  84. package/dist/rule.d.ts +3 -0
  85. package/dist/rule.d.ts.map +1 -1
  86. package/dist/rule.js.map +1 -1
  87. package/dist/rules/biome.js +2 -2
  88. package/dist/rules/biome.js.map +1 -1
  89. package/dist/rules/built-in.d.ts +1 -1
  90. package/dist/rules/built-in.d.ts.map +1 -1
  91. package/dist/rules/built-in.js +4 -0
  92. package/dist/rules/built-in.js.map +1 -1
  93. package/dist/rules/maxDepth.d.ts +2 -0
  94. package/dist/rules/maxDepth.d.ts.map +1 -0
  95. package/dist/rules/maxDepth.js +20 -0
  96. package/dist/rules/maxDepth.js.map +1 -0
  97. package/dist/rules/noBarrel.d.ts +2 -0
  98. package/dist/rules/noBarrel.d.ts.map +1 -0
  99. package/dist/rules/noBarrel.js +14 -0
  100. package/dist/rules/noBarrel.js.map +1 -0
  101. package/dist/rules/suppressions/biome/ConsumerDeclaration.d.ts +7 -0
  102. package/dist/rules/suppressions/biome/ConsumerDeclaration.d.ts.map +1 -0
  103. package/dist/rules/suppressions/biome/ConsumerDeclaration.js +15 -0
  104. package/dist/rules/suppressions/biome/ConsumerDeclaration.js.map +1 -0
  105. package/dist/rules/suppressions/biome-overrides.d.ts.map +1 -1
  106. package/dist/rules/suppressions/biome-overrides.js +7 -16
  107. package/dist/rules/suppressions/biome-overrides.js.map +1 -1
  108. package/docs/debt-and-hooks.md +31 -11
  109. package/docs/modules.md +19 -1
  110. package/docs/north-star.md +4 -4
  111. package/docs/roadmap.md +2 -2
  112. package/docs/rules.md +21 -34
  113. package/docs/tooling.md +2 -2
  114. package/package.json +2 -2
  115. package/src/biome/exceptionConfig.ts +70 -0
  116. package/src/biome/report.ts +11 -5
  117. package/src/biome/run.ts +17 -10
  118. package/src/cli/args.ts +7 -29
  119. package/src/cli/fix.ts +14 -9
  120. package/src/cli/lint.ts +3 -1
  121. package/src/cli/messages.ts +31 -0
  122. package/src/cli/migrateExceptions.ts +87 -0
  123. package/src/cli/program.ts +50 -0
  124. package/src/cli/run-main.ts +17 -11
  125. package/src/cli.ts +2 -48
  126. package/src/config/decode.ts +2 -0
  127. package/src/config/load.ts +15 -1
  128. package/src/config/resolve.ts +12 -1
  129. package/src/config.ts +2 -0
  130. package/src/engine/run-rules.ts +21 -4
  131. package/src/engine/session.ts +11 -3
  132. package/src/exceptions/biomeFindings.ts +20 -0
  133. package/src/exceptions/declaration.ts +38 -0
  134. package/src/exceptions/migrateBiome.ts +78 -0
  135. package/src/exceptions/migrateConfig.ts +64 -0
  136. package/src/exceptions/registryEntries.ts +10 -0
  137. package/src/rule.ts +3 -0
  138. package/src/rules/biome.ts +2 -2
  139. package/src/rules/built-in.ts +4 -0
  140. package/src/rules/maxDepth.ts +24 -0
  141. package/src/rules/noBarrel.ts +16 -0
  142. package/src/rules/suppressions/biome/ConsumerDeclaration.ts +24 -0
  143. package/src/rules/suppressions/biome-overrides.ts +7 -26
@@ -33,7 +33,7 @@ quality lint
33
33
 
34
34
  ### Baseline growth
35
35
 
36
- An error-level finding that neither the registry nor the baseline covers fails `quality lint`; that is how new debt gets noticed. The baseline itself may grow: a reviewer sees each new or raised entry in the diff and judges it. When the failing findings are debt the baseline should keep, such as the debt of a file that moved or was renamed, record the rule again with `quality baseline write --rule <id>`. The report ends with that command for the rules that failed. Then call out the baseline growth in the pull request description.
36
+ An error-level finding that neither the config exceptions nor the baseline covers fails `quality lint`; that is how new debt gets noticed. The baseline itself may grow: a reviewer sees each new or raised entry in the diff and judges it. When the failing findings are debt the baseline should keep, such as the debt of a file that moved or was renamed, record the rule again with `quality baseline write --rule <id>`. The report ends with that command for the rules that failed. Then call out the baseline growth in the pull request description.
37
37
 
38
38
  When an upgrade adds a shared check, keep its default error level, inspect its findings and record retained debt with `quality baseline write --rule <id>`. Review any new counts in the commit.
39
39
 
@@ -41,19 +41,39 @@ When an upgrade adds a shared check, keep its default error level, inspect its f
41
41
 
42
42
  A count depends on the rule's limit, so a Quality policy change that adjusts a limit shifts every count of that rule. The public API also exposes numeric overrides; that does not make tuning thresholds an adoption step. Prefer the shared defaults. A looser limit leaves entries that allow more than is left: they pass, and `tighten` and `prune` lower them. A stricter limit can make an error-level rule's files fail `quality lint`, since each is now further over the limit. Inspect the findings under the new policy before recording retained debt again with `quality baseline write --rule <id>`. Commit the counts with the policy upgrade. The raised entries show in the diff, and the pull request calls them out.
43
43
 
44
- ## Registry
44
+ ## Exceptions
45
45
 
46
- ```json
47
- [
48
- {
49
- "rule": "structure/max-lines",
50
- "file": "src/generated/client.ts",
51
- "reason": "Generated from the OpenAPI document; splitting it would mean forking the generator."
52
- }
53
- ]
46
+ Declare permanent exceptions in `quality.config.ts`, in `exceptions`. One entry names `files`, `rules` and one `reason`; it can cover both Quality and Biome rules on the same file:
47
+
48
+ ```ts
49
+ export default defineConfig({
50
+ exceptions: [
51
+ {
52
+ files: ["src/parse.ts"],
53
+ reason: "This parser documents and returns the external format unchanged.",
54
+ rules: ["comments/no-jsdoc", "biome/lint/suspicious/noExplicitAny"],
55
+ },
56
+ ],
57
+ });
58
+ ```
59
+
60
+ Paths are explicit, relative to the config folder. A Quality exception can add `subject` to cover only that finding's subject. Rules that refuse exceptions still refuse them. Quality exceptions apply before the baseline and must cover a current finding; unknown rules, refused rules and stale entries fail the gate. Biome declarations permit individual lint rules and assist actions on explicit files, without a subject. A Biome declaration naming a missing file fails too.
61
+
62
+ Quality merges the native Biome overrides into a temporary config beside the root config, preserving the repository's settings, preset and plugins, and removes the temporary file after the command. Both `quality lint` and `quality fix` use it. Keep `biome.json` free of consumer exception settings; a manual weakening is still reported. Direct editor or Biome commands use the checked-in config; run Quality to apply its exceptions.
63
+
64
+ For a generated directory exclusion, use `rules: ["biome/files/includes"]` and `files: ["**", "!**/test-support/generated"]`. The [rule reference](./rules.md#declared-biome-overrides) describes the narrow directory pattern policy.
65
+
66
+ ### Upgrade existing exceptions
67
+
68
+ ```sh
69
+ quality exceptions migrate
70
+ quality fix
71
+ quality lint
54
72
  ```
55
73
 
56
- An entry covers a rule's violations in one file, for good, and must say why. With a `subject`, it covers only the violations with that subject. The registry applies before the baseline. An entry that covers nothing, or names a rule that is off, unknown or takes no exceptions, fails the gate until it is removed.
74
+ The command reads the legacy registry (the configured `registry` path, or `quality/registry.json`) and `suppressions/biome-overrides.options.declared`. It moves them into `exceptions`, merges declarations with the same files, reason and subject, deletes the old registry and removes the old rule setting and matching root Biome settings. It keeps other config properties. `--from <registry>` selects another legacy registry and `--config <file>` selects a config. Without a config it creates one.
75
+
76
+ Export an object or `defineConfig({ ... })` without spreads for automatic editing; inline the `rules` object when it contains old Biome declarations. Literal computed keys such as `["exceptions"]` are supported; replace dynamic computed keys with literal keys before migrating. Unsupported shapes fail before any writes. Review the diff and remove unused declaration imports and files. For declarations of settings in nested or extended local Biome configs, remove those settings too; automatic removal covers the root `biome.json` and `biome.jsonc`. Legacy readers keep existing consumers running during upgrades; new exceptions go in `exceptions`.
57
77
 
58
78
  ## Pre-commit hook
59
79
 
package/docs/modules.md CHANGED
@@ -30,9 +30,13 @@ A module link opens its source. Type and value declarations with the same name a
30
30
 
31
31
  | Module | Exported names |
32
32
  | --- | --- |
33
+ | [biome/exceptionConfig.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/biome/exceptionConfig.ts) | `exceptionConfig`, `withExceptionConfig` |
34
+ | [biome/nativeEffectCall.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/biome/nativeEffectCall.ts) | `nativeEffectCall` |
35
+ | [biome/recognizeDiagnostics.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/biome/recognizeDiagnostics.ts) | `recognizeDiagnostics` |
33
36
  | [biome/recommendedRules.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/biome/recommendedRules.ts) | `recommendedRules` |
34
37
  | [biome/report.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/biome/report.ts) | `Diagnostic`, `Report`, `biomeReport` |
35
38
  | [biome/run.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/biome/run.ts) | `BiomeRun`, `runBiome` |
39
+ | [biome/syntax.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/biome/syntax.ts) | `Syntax`, `syntaxOf`, `offsetOf`, `nodeAt` |
36
40
 
37
41
  ## cli
38
42
 
@@ -42,8 +46,11 @@ A module link opens its source. Type and value declarations with the same name a
42
46
  | [cli/baseline.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/cli/baseline.ts) | `writeBaseline`, `shrinkBaseline` |
43
47
  | [cli/fix.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/cli/fix.ts) | `MAX_FIX_ROUNDS`, `snapshotOf`, `Pass`, `Settling`, `settle`, `fix` |
44
48
  | [cli/lint.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/cli/lint.ts) | `lint` |
49
+ | [cli/messages.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/cli/messages.ts) | `USAGE`, `ADOPT_REMOVED` |
45
50
  | [cli/migrate.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/cli/migrate.ts) | `migrateBaseline` |
46
- | [cli/run-main.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/cli/run-main.ts) | `quietOnClosedPipe`, `runMain` |
51
+ | [cli/migrateExceptions.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/cli/migrateExceptions.ts) | `migrateExceptions` |
52
+ | [cli/program.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/cli/program.ts) | `Invocation`, `program` |
53
+ | [cli/run-main.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/cli/run-main.ts) | `exitCode`, `quietOnClosedPipe`, `services`, `explained`, `runMain` |
47
54
  | [cli/shrink.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/cli/shrink.ts) | `pruneBaseline`, `tightenBaseline` |
48
55
 
49
56
  ## config
@@ -69,6 +76,11 @@ A module link opens its source. Type and value declarations with the same name a
69
76
 
70
77
  | Module | Exported names |
71
78
  | --- | --- |
79
+ | [exceptions/biomeFindings.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/exceptions/biomeFindings.ts) | `missingBiomeFiles`, `biomeExceptionHint` |
80
+ | [exceptions/declaration.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/exceptions/declaration.ts) | `ExceptionDeclaration`, `decodeExceptions` |
81
+ | [exceptions/migrateBiome.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/exceptions/migrateBiome.ts) | `migrateBiome` |
82
+ | [exceptions/migrateConfig.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/exceptions/migrateConfig.ts) | `migrateConfig` |
83
+ | [exceptions/registryEntries.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/exceptions/registryEntries.ts) | `registryEntries` |
72
84
  | [exceptions/registry.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/exceptions/registry.ts) | `RegistryEntry`, `decodeRegistry`, `StaleRegistryEntry`, `RegistryCheck`, `applyRegistry` |
73
85
 
74
86
  ## git
@@ -95,6 +107,8 @@ A module link opens its source. Type and value declarations with the same name a
95
107
  | Module | Exported names |
96
108
  | --- | --- |
97
109
  | [imports/ambient.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/imports/ambient.ts) | `Ambient`, `ambientModules` |
110
+ | [imports/browserGlobals.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/imports/browserGlobals.ts) | `browserGlobals` |
111
+ | [imports/browserNames.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/imports/browserNames.ts) | `browserNames` |
98
112
  | [imports/components.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/imports/components.ts) | `cyclicComponents` |
99
113
  | [imports/departures.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/imports/departures.ts) | `Departure`, `departures` |
100
114
  | [imports/extract.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/imports/extract.ts) | `ImportKind`, `ImportRequest`, `importsOf` |
@@ -105,6 +119,7 @@ A module link opens its source. Type and value declarations with the same name a
105
119
  | [imports/pnpm-workspace.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/imports/pnpm-workspace.ts) | `pnpmWorkspacePatterns` |
106
120
  | [imports/projects.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/imports/projects.ts) | `Project`, `projectsFor` |
107
121
  | [imports/resolve.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/imports/resolve.ts) | `Endpoint`, `builtinName`, `packageNameOf`, `isDeclarationFile`, `presumedTarget`, `resolveImport` |
122
+ | [imports/runtime.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/imports/runtime.ts) | `Runtime`, `runtimeOf` |
108
123
  | [imports/specifiers.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/imports/specifiers.ts) | `SpecifierSite`, `specifierSites` |
109
124
  | [imports/workspace.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/imports/workspace.ts) | `WorkspacePackage`, `workspacePackages`, `packageOf` |
110
125
 
@@ -202,8 +217,10 @@ A module link opens its source. Type and value declarations with the same name a
202
217
  | [rules/imports/aliased.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/rules/imports/aliased.ts) | `importsAliased` |
203
218
  | [rules/imports/cycles.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/rules/imports/cycles.ts) | `importCycles` |
204
219
  | [rules/imports/fences.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/rules/imports/fences.ts) | `importFences` |
220
+ | [rules/imports/manifest.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/rules/imports/manifest.ts) | `importsManifest` |
205
221
  | [rules/imports/noTestCode.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/rules/imports/noTestCode.ts) | `noTestCode` |
206
222
  | [rules/imports/resolvable.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/rules/imports/resolvable.ts) | `importsResolvable` |
223
+ | [rules/imports/runtime.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/rules/imports/runtime.ts) | `importsRuntime` |
207
224
 
208
225
  ## rules/stories
209
226
 
@@ -228,6 +245,7 @@ A module link opens its source. Type and value declarations with the same name a
228
245
 
229
246
  | Module | Exported names |
230
247
  | --- | --- |
248
+ | [rules/suppressions/biome/ConsumerDeclaration.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/rules/suppressions/biome/ConsumerDeclaration.ts) | `ConsumerDeclaration` |
231
249
  | [rules/suppressions/biome/configs.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/rules/suppressions/biome/configs.ts) | `Preset`, `Layer`, `Chain`, `BiomeConfigs`, `biomeConfigs` |
232
250
  | [rules/suppressions/biome/declarations.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/rules/suppressions/biome/declarations.ts) | `Declaration`, `PRESET_DECLARATIONS`, `decodeShipped`, `Scoped`, `matches`, `scoped`, `scopeText` |
233
251
  | [rules/suppressions/biome/json.ts](https://github.com/ShivaeDev/platform/blob/main/packages/quality/src/rules/suppressions/biome/json.ts) | `Json`, `Parsed`, `parseJsonc`, `member`, `entriesOf`, `itemsOf`, `textOf`, `isFalse` |
@@ -12,7 +12,7 @@ A consuming repository gets the whole policy. It does not select a preset of sta
12
12
 
13
13
  The repository supplies facts Quality cannot infer: where the source and generated files live, which architectural boundaries its imports must respect, and which additional commands a commit must run. Shared Biome and TypeScript configuration and Vitest helpers carry the package's conventions into those tools. The usual setup should stay small, with defaults that make sense together.
14
14
 
15
- Adopting the whole policy does not require fixing all old debt in one change. The baseline records existing findings by rule and file while the checks stay strict. An uncovered finding asks for a repair. Fixing debt passes immediately, and lowering tools reduce its recorded counts. A permitted permanent exception lives in a registry with a reason. When that exception no longer applies, the gate asks for its removal.
15
+ Adopting the whole policy does not require fixing all old debt in one change. The baseline records existing findings by rule and file while the checks stay strict. An uncovered finding asks for a repair. Fixing debt passes immediately, and lowering tools reduce its recorded counts. A permitted permanent exception lives in the config with a reason, once for both Quality and Biome. When that exception no longer applies, the gate asks for its removal.
16
16
 
17
17
  Every finding should tell an agent that has never used the package what to change. It names the rule and identifies the file and line when available. Invalid input, an unreadable policy or an import boundary with contradictory examples must stop the run rather than make a check silently narrower.
18
18
 
@@ -21,7 +21,7 @@ Every finding should tell an agent that has never used the package what to chang
21
21
  - Adoption is a small setup task. A repository uses the shared defaults instead of maintaining its own catalogue of rules and levels.
22
22
  - Quality's rules, tool configuration and internal exceptions express one policy. A new shared convention belongs here, with a reason and accepted and rejected examples.
23
23
  - Repository-specific inputs describe code and architecture. A generated path or import fence does not become permission to choose a weaker standard.
24
- - The config, inventory, rule evaluation, registry, baseline and report form one path. Checks use that path instead of inventing their own debt handling.
24
+ - The config, inventory, rule evaluation, exceptions, baseline and report form one path. Checks use that path instead of inventing their own debt handling.
25
25
  - A rule defines exactly what it checks. CLI and git promises have real-command tests; package promises have installed-consumer tests. Messages never imply stronger guarantees than those tests prove.
26
26
  - Debt diffs are reviewable. Lowering one entry preserves unrelated lines, and a tracked move can carry the debt it already had.
27
27
  - The author retains choices the checker cannot make, including symbol names, semantic code order and whether a newly recorded finding is acceptable debt.
@@ -36,7 +36,7 @@ The public configuration surface and its gaps are recorded in the [roadmap](./ro
36
36
 
37
37
  ### Strictness and adoption
38
38
 
39
- Adopt the policy as a whole; record old debt instead of weakening it. The baseline exposes uncovered findings without demanding an immediate clean-up of every file. It is a record to review, not proof that retained debt is harmless. Explicit recording can raise counts, and a reviewer owns whether that growth is justified. A registry entry is for an enduring, explained exception a rule permits, not a substitute for following that rule.
39
+ Adopt the policy as a whole; record old debt instead of weakening it. The baseline exposes uncovered findings without demanding an immediate clean-up of every file. It is a record to review, not proof that retained debt is harmless. Explicit recording can raise counts, and a reviewer owns whether that growth is justified. A config exception is for an enduring, explained exception a rule permits, not a substitute for following that rule.
40
40
 
41
41
  ### Automation and judgement
42
42
 
@@ -56,6 +56,6 @@ A rule can require a test's name or report a fixture-writing call. It cannot est
56
56
  - A requirement for every repository to build its own rules or copy a complicated config before it can use Quality.
57
57
  - Application services, persistence, transactions, request identity, client state and other runtime framework jobs. Their owning packages implement them.
58
58
  - A replacement for TypeScript, Biome, Vitest or git.
59
- - Automatic approval of baseline growth or a registry reason. Review owns whether retained debt is justified.
59
+ - Automatic approval of baseline growth or a exception reason. Review owns whether retained debt is justified.
60
60
  - A promise that static imports reproduce every runtime loader or that syntax rules prove business behavior.
61
61
  - Renaming public symbols or changing APIs to satisfy a rule whose maintainer decision remains open.
package/docs/roadmap.md CHANGED
@@ -7,7 +7,7 @@ This page owns Quality's shared policy, tooling, CLI and hook implementation sta
7
7
  - [x] Optional typed and runtime-validated config with config-free defaults; built-in and local rules enforced at `error`; local-rule and Standard Schema option APIs; one checked inventory and one report grouped by rule.
8
8
  - [x] The public config rejects `warn`, `off`, raised thresholds, inventory exclusions and custom test-file patterns. Vitest projects reject suite exclusions. Reasoned Biome declarations cover individual rules on explicit files and named directory exclusions.
9
9
  - [x] A JSON Lines baseline with counted debt, explicit named-rule recording, lowering and pruning, tracked-move handling and preservation of unrelated lines.
10
- - [x] A registry of reasoned exceptions with optional subjects, detection of stale entries and rules that refuse exceptions.
10
+ - [x] One reasoned config exception list for Quality and Biome, generated native Biome settings at lint and fix time, legacy migration, optional Quality subjects, stale entries and rules that refuse exceptions.
11
11
  - [x] File-size and comment rules; inline-suppression, double-cast, deprecation-setting and declared-Biome-override checks.
12
12
  - [x] A shared Biome preset and bridge whose error findings use the gate's baseline; enforcement of the installed Biome's recommended rules; bounded fix rounds and selected fixes left to the author.
13
13
  - [x] Static import resolution, rejection of production imports of test code, runtime-cycle checks, same-folder relative imports and direct, transitive and vocabulary fences with validated examples.
@@ -16,12 +16,12 @@ This page owns Quality's shared policy, tooling, CLI and hook implementation sta
16
16
  - [x] Opt-in pre-commit hooks with per-worktree config and dependencies, conventional `typecheck` and `quality:commit` scripts, and explicit installation.
17
17
  - [x] Seeded-repository and real-command tests for the gate, import policies, Biome, baseline and git hooks, plus packed consumer checks for the CLI and presets.
18
18
  - [x] Separate shipped Biome declarations from consumer exceptions; reject whole-check, group and blanket-scope consumer declarations.
19
+ - [x] Shared nesting-depth and index-module checks; Platform's catalog, manifest and Codecov checks use the local-rule API and the same inventory, report and baseline.
19
20
 
20
21
  ## Next
21
22
 
22
23
  - [ ] Remove obsolete CLI warning-report controls; config levels are already enforced at `error`. Keep old-debt adoption in the baseline.
23
24
  - [ ] Reduce policy-tuning inputs to the ones checks need about a repository. Thresholds can only be lowered; local-rule APIs remain exposed. Review the remaining extension boundary.
24
- - [ ] Port reusable checks still in `script/lint/rules/` into the shared policy, so consumers do not copy Platform's scripts.
25
25
  - [ ] Split Grit implementation modules by the checks they own, without making consumers select competing presets; route Platform's remaining static checks through Quality.
26
26
  - [ ] Define the Effect-boundary rule's source and test scope before porting the repository's `try`/`async` restrictions.
27
27
  - [ ] Define the service-parameter policy before enforcing it against public helpers that accept services or `Context`.
package/docs/rules.md CHANGED
@@ -2,10 +2,18 @@
2
2
 
3
3
  Use the [README](../README.md) for the gate's mental model and workflow. Quality owns the shared policy; consuming repositories adopt its defaults. This reference gives each built-in rule's scope, findings and accepted options, including the exposed local-rule API. An option being accepted is not a recommendation to tune the shared standard. Supply repository facts, such as generated locations and import boundaries, when a check needs them. The [naming reference](./naming.md) covers names and test layout; [shared tooling](./tooling.md) covers the Biome, Vitest and TypeScript setup.
4
4
 
5
- ## File size
5
+ ## File structure
6
+
7
+ ### File size
6
8
 
7
9
  `structure/max-lines` keeps each module to one job: a source file may have 150 lines and [test code](./naming.md#test-code) 300, counted the way an editor numbers them. Declaration files are exempt. Its options are `source` and `test`: positive integer limits no greater than 150 and 300 respectively. Test code is identified by its conventional name or `test-support` folder; custom test-file patterns are rejected. A file over its limit counts one violation for each line above it: 168 lines under a limit of 150 count 18.
8
10
 
11
+ ### Nesting and module structure
12
+
13
+ `nesting/max-depth` reports each source line that starts with eight or more tabs. Extract a named function or component. Declaration files are exempt; indentation with spaces is outside this text check. It takes no options.
14
+
15
+ `structure/no-barrel` rejects every source module named `index.ts` or `index.tsx`, including modules that define their own exports. Name the module after its purpose. It takes no options. The shared Biome preset separately rejects re-exports regardless of the file name.
16
+
9
17
  ### Comments
10
18
 
11
19
  A comment says why, never what the code already says or what it used to be. Seven rules hold every comment to that:
@@ -49,7 +57,7 @@ A repository with existing comments adopts the rules through the baseline, for e
49
57
 
50
58
  ### Suppressions
51
59
 
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.
60
+ 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
61
 
54
62
  | Rule | Reports | Options |
55
63
  | --- | --- | --- |
@@ -79,7 +87,9 @@ A type test proves that an API rejects what its types forbid, and TypeScript ass
79
87
 
80
88
  #### Declared Biome overrides
81
89
 
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.
90
+ 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).
91
+
92
+ 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
93
 
84
94
  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
95
 
@@ -99,30 +109,7 @@ Quality ships reasons for its own internal Biome exceptions. Extend the shared c
99
109
 
100
110
  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
111
 
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.
112
+ 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
113
 
127
114
  #### Shared presets
128
115
 
@@ -134,7 +121,7 @@ A config `extends` its entries the way Biome does:
134
121
  - 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
122
  - 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
123
 
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:
124
+ 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
125
 
139
126
  ```json
140
127
  [
@@ -146,7 +133,7 @@ A package that ships a preset declares the weakenings the preset makes in a `dec
146
133
  ]
147
134
  ```
148
135
 
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.
136
+ 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
137
 
151
138
  #### Recommended Biome rules
152
139
 
@@ -181,7 +168,7 @@ export default defineConfig({
181
168
  });
182
169
  ```
183
170
 
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.
171
+ `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
172
 
186
173
  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
174
 
@@ -254,13 +241,13 @@ The DSL does not compile without `demonstratedBy`, and the rule checks the examp
254
241
 
255
242
  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
243
 
257
- A finding has its fence's name as its subject, so a registry entry with that subject excuses one file from one fence.
244
+ A finding has its fence's name as its subject, so a config exception with that subject excuses one file from one fence.
258
245
 
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.
246
+ `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
247
 
261
248
  ### Manifests
262
249
 
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.
250
+ `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
251
 
265
252
  ## Local-rule API
266
253
 
@@ -307,4 +294,4 @@ export const limited = defineRule({
307
294
  });
308
295
  ```
309
296
 
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.
297
+ 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.1",
3
+ "version": "0.23.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": {
@@ -62,7 +62,7 @@
62
62
  "@effect/platform-node": "4.0.4",
63
63
  "@effect/platform-node-shared": "4.0.4",
64
64
  "@effect/vitest": "4.0.4",
65
- "@types/node": "24.19.1",
65
+ "@types/node": "24.19.2",
66
66
  "@typescript/native": "npm:typescript@7.0.2",
67
67
  "effect": "4.0.4",
68
68
  "vitest": "5.0.3"
@@ -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":