@shivaedev/quality 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.15.0 - 2026-10-05
4
+
5
+ ### Added
6
+
7
+ - `suppressions/biome-recommended` reports each rule the installed Biome recommends that the shared preset neither sets to `error` nor declares in its `declarations.json`, so a Biome upgrade that adds or changes a recommended rule fails the gate instead of running at Biome's default level. It asks Biome itself for the recommended set, through `biome rage --linter` under the `recommended` preset, and each finding names the rule and says to set it to `error` or declare why not. It takes no registry exceptions. The shipped preset passes it, so a repository that upgrades has nothing to record.
8
+
9
+ ## 0.14.0 - 2026-10-05
10
+
11
+ ### Added
12
+
13
+ - The `effect-fn-functions` plugin reports a top-level function whose whole body is `Effect.gen(…)`, written as an arrow or as a function that only returns it. Write it as `const loadItem = Effect.fn("ItemStore.loadItem")(function* (id: string) { … })`, which names its span after the function, or as `Effect.fnUntraced(…)` when it needs no span. A function nested in another, an object member, a `.pipe(…)` on the `Effect.gen` and a function that runs other statements before it are not reported. The rule has no fix.
14
+ - The `effect-test-bodies` plugin reports, in test code, a callback whose whole body is `Effect.gen(function* () { … })` passed to `it`, `test` or one of their members, such as `it.effect` or `it.effect.skipIf(…)`. Pass the generator to the test instead, `it.effect("loads the item", function* () { … })`, and let the test helper run it with `Effect.gen`; `@shivaedev/effect-test/it.ts` provides such an `it`. Test code is a test file or a file under `test-support/`, as for every other rule. The rule has no fix.
15
+ - A repository that upgrades records the existing findings of the two plugins with `quality baseline write --rule biome/plugin`.
16
+
3
17
  ## 0.13.0 - 2026-10-05
4
18
 
5
19
  ### Changed
package/README.md CHANGED
@@ -101,13 +101,14 @@ A repository with existing comments adopts the rules through the baseline, for e
101
101
 
102
102
  ### Suppressions
103
103
 
104
- A check that is silenced at one site hides the problem instead of fixing it. Four 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.
104
+ 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.
105
105
 
106
106
  | Rule | Reports | Options |
107
107
  | --- | --- | --- |
108
108
  | `suppressions/no-inline` | Every comment directive that silences a linter, the compiler or a formatter, except `@ts-expect-error` in a type test | none |
109
109
  | `suppressions/no-double-cast` | A cast through `unknown`, `any` or `never`: `x as unknown as T`, `x as any as T`, `x as never as T`, `<T><unknown>x` | none |
110
110
  | `suppressions/biome-overrides` | A Biome setting that turns a check off or down, or keeps files out of it, without a declaration, and a declaration that matches no setting | `declared` |
111
+ | `suppressions/biome-recommended` | A rule the installed Biome recommends that the [Biome preset](#biome-preset) neither sets to `error` nor declares | none |
111
112
  | `suppressions/no-ignore-deprecations` | `compilerOptions.ignoreDeprecations` in a tsconfig, which silences TypeScript's errors for deprecated options | none |
112
113
 
113
114
  `suppressions/no-inline` reports these directives, wherever a line of a comment starts with one:
@@ -197,6 +198,10 @@ A package that ships a preset declares the weakenings the preset makes in a `dec
197
198
 
198
199
  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.
199
200
 
201
+ #### Recommended Biome rules
202
+
203
+ `suppressions/biome-recommended` holds the [Biome preset](#biome-preset) to every rule Biome recommends, so a Biome upgrade that adds a recommended rule, or recommends one the preset leaves below `error`, fails the gate until the preset takes a position on it. It asks the installed Biome, the one `quality` runs, which rules its `recommended` preset enables, through `biome rage --linter`, so the list is never written down. When the root Biome config extends `@shivaedev/quality/biome`, it reads that preset and its `declarations.json`, resolved from the repository root as under [Shared presets](#shared-presets); a repository that does not extend the preset is left to the `biome` rule, which asks for it. A recommended rule is in order when the preset's `linter.rules` sets it to `error`, directly or through its group's severity, or when a declaration names it, whatever the declaration's `includes`. Every other recommended rule is reported at the preset, one finding per rule: set it to `error`, or declare why not.
204
+
200
205
  ### Imports
201
206
 
202
207
  Four rules read the imports of the TypeScript and JavaScript modules among the sources. They build it once per run with the TypeScript compiler, without a bundler or another dependency.
@@ -397,7 +402,7 @@ The preset sets:
397
402
  - **Plugins:** GritQL rules that ban ambient time, randomness, `console` and `process.env` for Effect's services, and the naming plugins under [Naming](#naming). They load from `./node_modules/@shivaedev/quality/biome/plugins`, so the package must be installed at the repository root.
398
403
  - Files ignored by git are skipped.
399
404
 
400
- The preset turns off `noUnusedVariables` and `noUnusedFunctionParameters`, because the tsconfig presets report them through TypeScript, allows default exports in `*.config.*` files, which tools load through the default export, and turns off `useComponentExportOnlyModules` in `*.test.*` and `*.spec.*` files, which define the helper and harness components they render. It also turns off `noProcessGlobal`, `useJsonImportAttributes`, `noMisusedPromises`, `useExhaustiveSwitchCases`, `useSortedClasses`, `noDelete`, `useConsistentArrayType`, `useConsistentCurlyBraces`, `noEqualsToNull` and `noSkippedTests`, because `quality fix` applies every lint fix and their fixes changed behavior or did not terminate on real code. It keeps `noUndeclaredClasses` off, because the rule cannot resolve a stylesheet imported through an alias and Tailwind utilities are not declared in CSS, and `noInlineStyles` off, because an inline style is the right tool for a value computed at run time. It declares these weakenings in its `declarations.json`, so `suppressions/biome-overrides` takes them as declared. Every other weakening a repository adds is an override it declares with a reason.
405
+ The preset turns off `noUnusedVariables` and `noUnusedFunctionParameters`, because the tsconfig presets report them through TypeScript, allows default exports in `*.config.*` files, which tools load through the default export, and turns off `useComponentExportOnlyModules` in `*.test.*` and `*.spec.*` files, which define the helper and harness components they render. It also turns off `noProcessGlobal`, `useJsonImportAttributes`, `noMisusedPromises`, `useExhaustiveSwitchCases`, `useSortedClasses`, `noDelete`, `useConsistentArrayType`, `useConsistentCurlyBraces`, `noEqualsToNull` and `noSkippedTests`, because `quality fix` applies every lint fix and their fixes changed behavior or did not terminate on real code. It keeps `noUndeclaredClasses` off, because the rule cannot resolve a stylesheet imported through an alias and Tailwind utilities are not declared in CSS, and `noInlineStyles` off, because an inline style is the right tool for a value computed at run time. It declares these weakenings in its `declarations.json`, so `suppressions/biome-overrides` takes them as declared and `suppressions/biome-recommended` accepts the recommended rules among them. Every other weakening a repository adds is an override it declares with a reason.
401
406
 
402
407
  An autofix applies only a change that removes no decision; a fix that can change behavior or delete something written on purpose reports only, and the author decides. The preset keeps `noAccessKey`, `noAriaHiddenOnFocusable`, `noAutofocus`, `noInteractiveElementToNoninteractiveRole`, `noNoninteractiveElementToInteractiveRole`, `noNoninteractiveTabindex`, `noRedundantRoles`, `useValidAriaProps`, `useValidAriaRole`, `noImportantStyles`, `noConstAssign`, `noUnusedPrivateClassMembers`, `useExhaustiveDependencies`, `noFloatingPromises`, `useConsistentTestIt`, `useRegexpTest`, `useUnicodeRegex`, `noNonNullAssertion`, `useAtIndex`, `noParametersOnlyUsedInRecursion` and `useNamingConvention` at `error` with their fixes off, and `quality fix` skips `noDuplicateObjectKeys` and `useSortedKeys`, whose fixes Biome's config cannot turn off.
403
408
 
@@ -422,6 +427,8 @@ A name says what a thing is, and the same kind of thing is named the same way ev
422
427
  | Schemas | PascalCase and named like their type: `const Item = Schema.Struct(...)` with `type Item = typeof Item.Type`. | `schema-names` plugin |
423
428
  | `Schema.Struct` fields | camelCase, also when quoted. Outside data keeps its keys only at the edge: `Schema.Struct({ createdAt: Schema.String }).pipe(Schema.encodeKeys({ createdAt: "created_at" }))`. | `schema-struct-keys` plugin, `key-names` plugin |
424
429
  | `Effect.fn` spans | `"Owner.operation"`, where the owner is the service or module and the operation is the name the function is bound to: `const loadItem = Effect.fn("ItemStore.loadItem")`, and `loadItem: Effect.fn("ItemStore.loadItem")` in an object. | `effect-fn-spans` plugin |
430
+ | `Effect.gen` functions | A top-level function whose whole body is `Effect.gen` is an `Effect.fn`: `const loadItem = Effect.fn("ItemStore.loadItem")(function* (id: string) { … })`, or `Effect.fnUntraced` when it needs no span. | `effect-fn-functions` plugin |
431
+ | Effect test bodies | In test code, a test takes the generator itself: `it.effect("loads the item", function* () { … })`, never `() => Effect.gen(function* () { … })`. The test helper runs it with `Effect.gen`. | `effect-test-bodies` plugin |
425
432
  | Service Layers | A static `layer` on the service, read as `ItemStore.layer`. No exported `ItemStoreLive` or `ItemStoreLayer` constant. A Layer that wires an application together stays unexported. | `service-layers` plugin |
426
433
  | Private members | A `#field`. No `private`, `protected` or `public` modifier. | `useConsistentMemberAccessibility` |
427
434
  | React | A function passed to an `onX` prop is `handleX`, an `onX` prop forwarded as it is, or a state setter passed as it is: `onOpenChange={setOpen}`. A context is `ThemeContext`, a ref `inputRef`, an id `fieldId`. A module that exports a component exports only components; a test may define the components it renders. | `handler-names` plugin, `useReactNamingConvention`, `useComponentExportOnlyModules` |
@@ -0,0 +1,15 @@
1
+ engine biome(1.0)
2
+ language js(typescript, jsx)
3
+
4
+ or {
5
+ JsVariableDeclarator(id=$name, initializer=JsInitializerClause(expression=JsArrowFunctionExpression(body=$body))),
6
+ JsFunctionDeclaration(id=$name, body=$body),
7
+ JsFunctionExportDefaultDeclaration(id=$name, body=$body)
8
+ } as $function where {
9
+ $body <: or {
10
+ JsFunctionBody(statements=[JsReturnStatement(argument=`Effect.gen($...)`)]),
11
+ `Effect.gen($...)`
12
+ },
13
+ ! $function <: within JsFunctionBody() ,
14
+ register_diagnostic(span=$name, message="A top-level function whose whole body is Effect.gen is an Effect.fn, which names its span and keeps the call site in traces: const loadItem = Effect.fn('ItemStore.loadItem')(function* (id: string) { ... }). Effect.fnUntraced fits a function that needs no span.")
15
+ }
@@ -0,0 +1,12 @@
1
+ engine biome(1.0)
2
+ language js(typescript, jsx)
3
+
4
+ `$test($title, $callback, $...)` where {
5
+ $test <: r"it|test|it\.[A-Za-z.]+|test\.[A-Za-z.]+|it\.[A-Za-z.]+\(.*\)|test\.[A-Za-z.]+\(.*\)",
6
+ $callback <: JsArrowFunctionExpression(body=$body),
7
+ $body <: or {
8
+ `Effect.gen(function* () { $... })`,
9
+ JsFunctionBody(statements=[JsReturnStatement(argument=`Effect.gen(function* () { $... })`)])
10
+ },
11
+ register_diagnostic(span=$callback, message="A test body that is only Effect.gen is the generator itself: pass the function* to the test, as in it.effect('loads the item', function* () { ... }), and the test helper runs it with Effect.gen. @shivaedev/effect-test/it.ts provides an it whose effect and live testers take one.")
12
+ }
package/biome/preset.json CHANGED
@@ -647,6 +647,14 @@
647
647
  "includes": ["**/*.ts", "**/*.tsx", "!**/*.d.ts"],
648
648
  "path": "./node_modules/@shivaedev/quality/biome/plugins/effect-fn-spans.grit"
649
649
  },
650
+ {
651
+ "includes": ["**/*.ts", "**/*.tsx", "!**/*.d.ts"],
652
+ "path": "./node_modules/@shivaedev/quality/biome/plugins/effect-fn-functions.grit"
653
+ },
654
+ {
655
+ "includes": ["**/*.test.*", "**/*.spec.*", "**/test-support/**", "!**/*.d.ts"],
656
+ "path": "./node_modules/@shivaedev/quality/biome/plugins/effect-test-bodies.grit"
657
+ },
650
658
  {
651
659
  "includes": ["**/*.ts", "**/*.tsx", "!**/*.d.ts"],
652
660
  "path": "./node_modules/@shivaedev/quality/biome/plugins/constant-names.grit"
@@ -0,0 +1,2 @@
1
+ export declare function recommendedRules(): Promise<readonly string[]>;
2
+ //# sourceMappingURL=recommendedRules.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recommendedRules.d.ts","sourceRoot":"","sources":["../../src/biome/recommendedRules.ts"],"names":[],"mappings":"AASA,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAanE"}
@@ -0,0 +1,22 @@
1
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { runBiome } from "./run.js";
5
+ const RECOMMENDED_ONLY = '{ "linter": { "rules": { "preset": "recommended" } } }\n';
6
+ const ENABLED_RULE = /^ {4}(?<rule>[a-z0-9]+\/\w+)$/gimu;
7
+ export async function recommendedRules() {
8
+ const directory = await mkdtemp(join(tmpdir(), "quality-biome-recommended-"));
9
+ try {
10
+ await writeFile(join(directory, "biome.json"), RECOMMENDED_ONLY);
11
+ const rage = await runBiome(directory, ["rage", "--linter", `--config-path=${directory}`]);
12
+ const rules = [...rage.stdout.matchAll(ENABLED_RULE)].flatMap((match) => match.groups?.rule ?? []);
13
+ if (rules.length === 0) {
14
+ throw new Error(`biome rage --linter listed no enabled rules under the recommended preset:\n${[rage.stdout, rage.stderr].join("\n").trim()}`);
15
+ }
16
+ return rules;
17
+ }
18
+ finally {
19
+ await rm(directory, { force: true, recursive: true });
20
+ }
21
+ }
22
+ //# sourceMappingURL=recommendedRules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recommendedRules.js","sourceRoot":"","sources":["../../src/biome/recommendedRules.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEpC,MAAM,gBAAgB,GAAG,0DAA0D,CAAC;AAEpF,MAAM,YAAY,GAAG,mCAAmC,CAAC;AAEzD,MAAM,CAAC,KAAK,UAAU,gBAAgB;IACrC,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,4BAA4B,CAAC,CAAC,CAAC;IAC9E,IAAI,CAAC;QACJ,MAAM,SAAS,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,EAAE,gBAAgB,CAAC,CAAC;QACjE,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,SAAS,EAAE,CAAC,MAAM,EAAE,UAAU,EAAE,iBAAiB,SAAS,EAAE,CAAC,CAAC,CAAC;QAC3F,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC;QACnG,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,8EAA8E,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QAC/I,CAAC;QACD,OAAO,KAAK,CAAC;IACd,CAAC;YAAS,CAAC;QACV,MAAM,EAAE,CAAC,SAAS,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACvD,CAAC;AACF,CAAC"}
@@ -1,4 +1,10 @@
1
- import { type Rule } from "#rule.ts";
1
+ import { type Rule, type RuleInputs } from "#rule.ts";
2
2
  export declare const PRESET = "@shivaedev/quality/biome";
3
+ interface RootConfig {
4
+ readonly extendsPreset: boolean;
5
+ readonly path: string;
6
+ }
7
+ export declare function rootConfig(readText: RuleInputs["readText"]): Promise<RootConfig>;
3
8
  export declare const biome: Rule<"biome", undefined>;
9
+ export {};
4
10
  //# sourceMappingURL=biome.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"biome.d.ts","sourceRoot":"","sources":["../../src/rules/biome.ts"],"names":[],"mappings":"AACA,OAAO,EAA4B,KAAK,IAAI,EAAmB,MAAM,UAAU,CAAC;AAGhF,eAAO,MAAM,MAAM,6BAA6B,CAAC;AA2CjD,eAAO,MAAM,KAAK,EAAE,IAAI,CAAC,OAAO,EAAE,SAAS,CAA+B,CAAC"}
1
+ {"version":3,"file":"biome.d.ts","sourceRoot":"","sources":["../../src/rules/biome.ts"],"names":[],"mappings":"AACA,OAAO,EAA4B,KAAK,IAAI,EAAE,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AAGhF,eAAO,MAAM,MAAM,6BAA6B,CAAC;AAsBjD,UAAU,UAAU;IACnB,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB;AAED,wBAAsB,UAAU,CAAC,QAAQ,EAAE,UAAU,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,CAMtF;AAkBD,eAAO,MAAM,KAAK,EAAE,IAAI,CAAC,OAAO,EAAE,SAAS,CAA+B,CAAC"}
@@ -18,14 +18,16 @@ function findingOf(diagnostic) {
18
18
  subject: diagnostic.category ?? "internal",
19
19
  };
20
20
  }
21
- async function presetFinding(readText) {
21
+ export async function rootConfig(readText) {
22
22
  const texts = await Promise.all(ROOT_CONFIGS.map(async (path) => ({ path, text: await readText(path) })));
23
23
  const found = texts.find((candidate) => candidate.text !== undefined);
24
24
  const parsed = found?.text === undefined ? undefined : parseJsonc(found.path, found.text);
25
25
  const extended = parsed?._tag === "Parsed" ? itemsOf(member(parsed.json, "extends")).map(textOf) : [];
26
- return extended.includes(PRESET)
27
- ? []
28
- : [{ file: found?.path ?? "biome.json", message: `Extend "${PRESET}" from the root Biome config.`, subject: "preset" }];
26
+ return { extendsPreset: extended.includes(PRESET), path: found?.path ?? "biome.json" };
27
+ }
28
+ async function presetFinding(readText) {
29
+ const { extendsPreset, path } = await rootConfig(readText);
30
+ return extendsPreset ? [] : [{ file: path, message: `Extend "${PRESET}" from the root Biome config.`, subject: "preset" }];
29
31
  }
30
32
  const bridge = defineRule({
31
33
  check: async ({ readText, root }) => {
@@ -1 +1 @@
1
- {"version":3,"file":"biome.js","sourceRoot":"","sources":["../../src/rules/biome.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"biome.js","sourceRoot":"","sources":["../../src/rules/biome.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAmB,MAAM,kBAAkB,CAAC;AAChE,OAAO,EAAE,UAAU,EAA4C,MAAM,UAAU,CAAC;AAChF,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,mCAAmC,CAAC;AAExF,MAAM,CAAC,MAAM,MAAM,GAAG,0BAA0B,CAAC;AAEjD,MAAM,YAAY,GAAsB,CAAC,YAAY,EAAE,aAAa,CAAC,CAAC;AAEtE,MAAM,OAAO,GAAwB,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;AAEjE,SAAS,SAAS,CAAC,UAAsB;IACxC,OAAO,UAAU,CAAC,QAAQ,KAAK,QAAQ,IAAI,UAAU,CAAC,OAAO,CAAC,UAAU,CAAC,8BAA8B,CAAC;QACvG,CAAC,CAAC,sCAAsC;QACxC,CAAC,CAAC,UAAU,CAAC,OAAO,CAAC;AACvB,CAAC;AAED,SAAS,SAAS,CAAC,UAAsB;IACxC,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,KAAK,EAAE,IAAI,IAAI,CAAC,CAAC;IAClD,OAAO;QACN,IAAI,EAAE,UAAU,CAAC,QAAQ,CAAC,IAAI,IAAI,GAAG;QACrC,IAAI,EAAE,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS;QACjC,OAAO,EAAE,SAAS,CAAC,UAAU,CAAC;QAC9B,OAAO,EAAE,UAAU,CAAC,QAAQ,IAAI,UAAU;KAC1C,CAAC;AACH,CAAC;AAOD,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,QAAgC;IAChE,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC1G,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC;IACtE,MAAM,MAAM,GAAG,KAAK,EAAE,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;IAC1F,MAAM,QAAQ,GAAG,MAAM,EAAE,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACtG,OAAO,EAAE,aAAa,EAAE,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,IAAI,YAAY,EAAE,CAAC;AACxF,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,QAAgC;IAC5D,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,GAAG,MAAM,UAAU,CAAC,QAAQ,CAAC,CAAC;IAC3D,OAAO,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,MAAM,+BAA+B,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC;AAC5H,CAAC;AAED,MAAM,MAAM,GAAG,UAAU,CAAC;IACzB,KAAK,EAAE,KAAK,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,EAAE;QACnC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,aAAa,CAAC,QAAQ,CAAC,EAAE,WAAW,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;QACpG,OAAO,CAAC,GAAG,MAAM,EAAE,GAAG,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC;IACnH,CAAC;IACD,WAAW,EACV,sKAAsK;IACvK,EAAE,EAAE,OAAO;IACX,WAAW,EAAE,KAAK;CAClB,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,KAAK,GAA6B,EAAE,GAAG,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC"}
@@ -13,7 +13,7 @@ export declare const builtInRules: readonly [import("../rule.ts").Rule<"structur
13
13
  readonly reason: string;
14
14
  readonly rule: string;
15
15
  }[];
16
- }>, import("../rule.ts").Rule<"biome", undefined>, import("../rule.ts").Rule<"imports/cycles", undefined>, import("../rule.ts").Rule<"imports/fences", {
16
+ }>, import("../rule.ts").Rule<"suppressions/biome-recommended", undefined>, import("../rule.ts").Rule<"biome", undefined>, import("../rule.ts").Rule<"imports/cycles", undefined>, import("../rule.ts").Rule<"imports/fences", {
17
17
  readonly fences?: readonly import("../imports/fences/model.ts").Fence[];
18
18
  }>, import("../rule.ts").Rule<"imports/resolvable", {
19
19
  readonly generated?: readonly string[];
@@ -1 +1 @@
1
- {"version":3,"file":"built-in.d.ts","sourceRoot":"","sources":["../../src/rules/built-in.ts"],"names":[],"mappings":"AAwBA,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwBf,CAAC"}
1
+ {"version":3,"file":"built-in.d.ts","sourceRoot":"","sources":["../../src/rules/built-in.ts"],"names":[],"mappings":"AAyBA,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyBf,CAAC"}
@@ -13,6 +13,7 @@ import { importCycles } from "#rules/imports/cycles.ts";
13
13
  import { importFences } from "#rules/imports/fences.ts";
14
14
  import { importsResolvable } from "#rules/imports/resolvable.ts";
15
15
  import { biomeOverrides } from "#rules/suppressions/biome-overrides.ts";
16
+ import { biomeRecommended } from "#rules/suppressions/biomeRecommended.ts";
16
17
  import { noDoubleCast } from "#rules/suppressions/no-double-cast.ts";
17
18
  import { noInline } from "#rules/suppressions/no-inline.ts";
18
19
  import { noIgnoreDeprecations } from "#rules/suppressions/noIgnoreDeprecations.ts";
@@ -34,6 +35,7 @@ export const builtInRules = [
34
35
  noDoubleCast,
35
36
  noIgnoreDeprecations,
36
37
  biomeOverrides,
38
+ biomeRecommended,
37
39
  biome,
38
40
  importCycles,
39
41
  importFences,
@@ -1 +1 @@
1
- {"version":3,"file":"built-in.js","sourceRoot":"","sources":["../../src/rules/built-in.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAC7D,OAAO,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAE,MAAM,0CAA0C,CAAC;AAC/E,OAAO,EAAE,OAAO,EAAE,MAAM,6BAA6B,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,sCAAsC,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACnE,OAAO,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACpE,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,EAAE,cAAc,EAAE,MAAM,wCAAwC,CAAC;AACxE,OAAO,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAC;AACrE,OAAO,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,6CAA6C,CAAC;AACnF,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAChE,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC1D,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE1C,MAAM,CAAC,MAAM,YAAY,GAAG;IAC3B,QAAQ;IACR,OAAO;IACP,eAAe;IACf,aAAa;IACb,QAAQ;IACR,MAAM;IACN,mBAAmB;IACnB,UAAU;IACV,QAAQ;IACR,YAAY;IACZ,oBAAoB;IACpB,cAAc;IACd,KAAK;IACL,YAAY;IACZ,YAAY;IACZ,iBAAiB;IACjB,cAAc;IACd,eAAe;IACf,gBAAgB;IAChB,WAAW;IACX,UAAU;IACV,WAAW;IACX,cAAc;CACL,CAAC"}
1
+ {"version":3,"file":"built-in.js","sourceRoot":"","sources":["../../src/rules/built-in.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAC7D,OAAO,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAE,MAAM,0CAA0C,CAAC;AAC/E,OAAO,EAAE,OAAO,EAAE,MAAM,6BAA6B,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,sCAAsC,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACnE,OAAO,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACpE,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,EAAE,cAAc,EAAE,MAAM,wCAAwC,CAAC;AACxE,OAAO,EAAE,gBAAgB,EAAE,MAAM,yCAAyC,CAAC;AAC3E,OAAO,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAC;AACrE,OAAO,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,6CAA6C,CAAC;AACnF,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAChE,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC1D,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE1C,MAAM,CAAC,MAAM,YAAY,GAAG;IAC3B,QAAQ;IACR,OAAO;IACP,eAAe;IACf,aAAa;IACb,QAAQ;IACR,MAAM;IACN,mBAAmB;IACnB,UAAU;IACV,QAAQ;IACR,YAAY;IACZ,oBAAoB;IACpB,cAAc;IACd,gBAAgB;IAChB,KAAK;IACL,YAAY;IACZ,YAAY;IACZ,iBAAiB;IACjB,cAAc;IACd,eAAe;IACf,gBAAgB;IAChB,WAAW;IACX,UAAU;IACV,WAAW;IACX,cAAc;CACL,CAAC"}
@@ -0,0 +1,4 @@
1
+ import { type Rule } from "#rule.ts";
2
+ export declare function biomeRecommendedFrom(recommended: () => Promise<readonly string[]>): Rule<"suppressions/biome-recommended", undefined>;
3
+ export declare const biomeRecommended: Rule<"suppressions/biome-recommended", undefined>;
4
+ //# sourceMappingURL=biomeRecommended.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"biomeRecommended.d.ts","sourceRoot":"","sources":["../../../src/rules/suppressions/biomeRecommended.ts"],"names":[],"mappings":"AAEA,OAAO,EAA4B,KAAK,IAAI,EAAmB,MAAM,UAAU,CAAC;AA6DhF,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC,gCAAgC,EAAE,SAAS,CAAC,CAQrI;AAED,eAAO,MAAM,gBAAgB,mDAAyC,CAAC"}
@@ -0,0 +1,62 @@
1
+ import { posix } from "node:path";
2
+ import { recommendedRules } from "#biome/recommendedRules.ts";
3
+ import { defineRule } from "#rule.ts";
4
+ import { PRESET, rootConfig } from "#rules/biome.ts";
5
+ import { decodeShipped, PRESET_DECLARATIONS } from "#rules/suppressions/biome/declarations.ts";
6
+ import { member, parseJsonc, textOf } from "#rules/suppressions/biome/json.ts";
7
+ import { resolvePackage } from "#rules/suppressions/biome/resolve.ts";
8
+ function settingOf(rules, rule) {
9
+ const [group = "", name = ""] = rule.split("/");
10
+ const groupNode = member(rules, group);
11
+ const severity = textOf(groupNode);
12
+ if (severity !== undefined) {
13
+ return { level: severity, line: groupNode?.line };
14
+ }
15
+ const node = member(groupNode, name);
16
+ return { level: textOf(node) ?? textOf(member(node, "level")), line: node?.line };
17
+ }
18
+ const FIX = `Set it to "error" in the preset, or declare why not in its ${PRESET_DECLARATIONS}.`;
19
+ function looseRule(file, rule, { level, line }) {
20
+ const state = level === undefined ? "leaves at Biome's default level" : `sets to "${level}"`;
21
+ return { file, line, message: `Biome recommends "lint/${rule}", which the preset ${state}. ${FIX}`, subject: `lint/${rule}` };
22
+ }
23
+ function problem(file, message) {
24
+ return [{ file, message }];
25
+ }
26
+ async function check(readText, recommended) {
27
+ const root = await rootConfig(readText);
28
+ if (!root.extendsPreset) {
29
+ return [];
30
+ }
31
+ const path = await resolvePackage(readText, PRESET);
32
+ if (path === undefined) {
33
+ return problem(root.path, `Cannot resolve "${PRESET}" from node_modules. Install @shivaedev/quality at the repository root.`);
34
+ }
35
+ const text = await readText(path);
36
+ const parsed = text === undefined ? undefined : parseJsonc(path, text);
37
+ if (parsed?._tag !== "Parsed") {
38
+ return problem(path, `Cannot read the preset "${PRESET}": ${parsed?.reason ?? "the file does not exist"}.`);
39
+ }
40
+ const declarationsPath = posix.join(posix.dirname(path), PRESET_DECLARATIONS);
41
+ const declarations = await decodeShipped(await readText(declarationsPath));
42
+ if (declarations._tag === "Invalid") {
43
+ return problem(declarationsPath, `Cannot read the declarations shipped with "${PRESET}": ${declarations.issues.join("; ")}.`);
44
+ }
45
+ const declared = new Set(declarations.value.map((declaration) => declaration.rule));
46
+ const rules = member(member(parsed.json, "linter"), "rules");
47
+ return (await recommended())
48
+ .filter((rule) => !declared.has(`lint/${rule}`))
49
+ .map((rule) => ({ rule, setting: settingOf(rules, rule) }))
50
+ .filter(({ setting }) => setting.level !== "error")
51
+ .map(({ rule, setting }) => looseRule(path, rule, setting));
52
+ }
53
+ export function biomeRecommendedFrom(recommended) {
54
+ return defineRule({
55
+ check: ({ readText }) => check(readText, recommended),
56
+ description: "Every rule the installed Biome recommends is at error in the shared preset, or the preset declares why not, so a Biome upgrade that adds or changes a recommended rule cannot slip in at Biome's default level.",
57
+ id: "suppressions/biome-recommended",
58
+ registrable: false,
59
+ });
60
+ }
61
+ export const biomeRecommended = biomeRecommendedFrom(recommendedRules);
62
+ //# sourceMappingURL=biomeRecommended.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"biomeRecommended.js","sourceRoot":"","sources":["../../../src/rules/suppressions/biomeRecommended.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shivaedev/quality",
3
- "version": "0.13.0",
3
+ "version": "0.15.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,23 @@
1
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { runBiome } from "./run.ts";
5
+
6
+ const RECOMMENDED_ONLY = '{ "linter": { "rules": { "preset": "recommended" } } }\n';
7
+
8
+ const ENABLED_RULE = /^ {4}(?<rule>[a-z0-9]+\/\w+)$/gimu;
9
+
10
+ export async function recommendedRules(): Promise<readonly string[]> {
11
+ const directory = await mkdtemp(join(tmpdir(), "quality-biome-recommended-"));
12
+ try {
13
+ await writeFile(join(directory, "biome.json"), RECOMMENDED_ONLY);
14
+ const rage = await runBiome(directory, ["rage", "--linter", `--config-path=${directory}`]);
15
+ const rules = [...rage.stdout.matchAll(ENABLED_RULE)].flatMap((match) => match.groups?.rule ?? []);
16
+ if (rules.length === 0) {
17
+ throw new Error(`biome rage --linter listed no enabled rules under the recommended preset:\n${[rage.stdout, rage.stderr].join("\n").trim()}`);
18
+ }
19
+ return rules;
20
+ } finally {
21
+ await rm(directory, { force: true, recursive: true });
22
+ }
23
+ }
@@ -24,14 +24,22 @@ function findingOf(diagnostic: Diagnostic): Finding {
24
24
  };
25
25
  }
26
26
 
27
- async function presetFinding(readText: RuleInputs["readText"]): Promise<readonly Finding[]> {
27
+ interface RootConfig {
28
+ readonly extendsPreset: boolean;
29
+ readonly path: string;
30
+ }
31
+
32
+ export async function rootConfig(readText: RuleInputs["readText"]): Promise<RootConfig> {
28
33
  const texts = await Promise.all(ROOT_CONFIGS.map(async (path) => ({ path, text: await readText(path) })));
29
34
  const found = texts.find((candidate) => candidate.text !== undefined);
30
35
  const parsed = found?.text === undefined ? undefined : parseJsonc(found.path, found.text);
31
36
  const extended = parsed?._tag === "Parsed" ? itemsOf(member(parsed.json, "extends")).map(textOf) : [];
32
- return extended.includes(PRESET)
33
- ? []
34
- : [{ file: found?.path ?? "biome.json", message: `Extend "${PRESET}" from the root Biome config.`, subject: "preset" }];
37
+ return { extendsPreset: extended.includes(PRESET), path: found?.path ?? "biome.json" };
38
+ }
39
+
40
+ async function presetFinding(readText: RuleInputs["readText"]): Promise<readonly Finding[]> {
41
+ const { extendsPreset, path } = await rootConfig(readText);
42
+ return extendsPreset ? [] : [{ file: path, message: `Extend "${PRESET}" from the root Biome config.`, subject: "preset" }];
35
43
  }
36
44
 
37
45
  const bridge = defineRule({
@@ -13,6 +13,7 @@ import { importCycles } from "#rules/imports/cycles.ts";
13
13
  import { importFences } from "#rules/imports/fences.ts";
14
14
  import { importsResolvable } from "#rules/imports/resolvable.ts";
15
15
  import { biomeOverrides } from "#rules/suppressions/biome-overrides.ts";
16
+ import { biomeRecommended } from "#rules/suppressions/biomeRecommended.ts";
16
17
  import { noDoubleCast } from "#rules/suppressions/no-double-cast.ts";
17
18
  import { noInline } from "#rules/suppressions/no-inline.ts";
18
19
  import { noIgnoreDeprecations } from "#rules/suppressions/noIgnoreDeprecations.ts";
@@ -35,6 +36,7 @@ export const builtInRules = [
35
36
  noDoubleCast,
36
37
  noIgnoreDeprecations,
37
38
  biomeOverrides,
39
+ biomeRecommended,
38
40
  biome,
39
41
  importCycles,
40
42
  importFences,
@@ -0,0 +1,74 @@
1
+ import { posix } from "node:path";
2
+ import { recommendedRules } from "#biome/recommendedRules.ts";
3
+ import { defineRule, type Finding, type Rule, type RuleInputs } from "#rule.ts";
4
+ import { PRESET, rootConfig } from "#rules/biome.ts";
5
+ import { decodeShipped, PRESET_DECLARATIONS } from "#rules/suppressions/biome/declarations.ts";
6
+ import { type Json, member, parseJsonc, textOf } from "#rules/suppressions/biome/json.ts";
7
+ import { resolvePackage } from "#rules/suppressions/biome/resolve.ts";
8
+
9
+ interface Setting {
10
+ readonly level: string | undefined;
11
+ readonly line: number | undefined;
12
+ }
13
+
14
+ function settingOf(rules: Json | undefined, rule: string): Setting {
15
+ const [group = "", name = ""] = rule.split("/");
16
+ const groupNode = member(rules, group);
17
+ const severity = textOf(groupNode);
18
+ if (severity !== undefined) {
19
+ return { level: severity, line: groupNode?.line };
20
+ }
21
+ const node = member(groupNode, name);
22
+ return { level: textOf(node) ?? textOf(member(node, "level")), line: node?.line };
23
+ }
24
+
25
+ const FIX = `Set it to "error" in the preset, or declare why not in its ${PRESET_DECLARATIONS}.`;
26
+
27
+ function looseRule(file: string, rule: string, { level, line }: Setting): Finding {
28
+ const state = level === undefined ? "leaves at Biome's default level" : `sets to "${level}"`;
29
+ return { file, line, message: `Biome recommends "lint/${rule}", which the preset ${state}. ${FIX}`, subject: `lint/${rule}` };
30
+ }
31
+
32
+ function problem(file: string, message: string): readonly Finding[] {
33
+ return [{ file, message }];
34
+ }
35
+
36
+ async function check(readText: RuleInputs["readText"], recommended: () => Promise<readonly string[]>): Promise<readonly Finding[]> {
37
+ const root = await rootConfig(readText);
38
+ if (!root.extendsPreset) {
39
+ return [];
40
+ }
41
+ const path = await resolvePackage(readText, PRESET);
42
+ if (path === undefined) {
43
+ return problem(root.path, `Cannot resolve "${PRESET}" from node_modules. Install @shivaedev/quality at the repository root.`);
44
+ }
45
+ const text = await readText(path);
46
+ const parsed = text === undefined ? undefined : parseJsonc(path, text);
47
+ if (parsed?._tag !== "Parsed") {
48
+ return problem(path, `Cannot read the preset "${PRESET}": ${parsed?.reason ?? "the file does not exist"}.`);
49
+ }
50
+ const declarationsPath = posix.join(posix.dirname(path), PRESET_DECLARATIONS);
51
+ const declarations = await decodeShipped(await readText(declarationsPath));
52
+ if (declarations._tag === "Invalid") {
53
+ return problem(declarationsPath, `Cannot read the declarations shipped with "${PRESET}": ${declarations.issues.join("; ")}.`);
54
+ }
55
+ const declared = new Set(declarations.value.map((declaration) => declaration.rule));
56
+ const rules = member(member(parsed.json, "linter"), "rules");
57
+ return (await recommended())
58
+ .filter((rule) => !declared.has(`lint/${rule}`))
59
+ .map((rule) => ({ rule, setting: settingOf(rules, rule) }))
60
+ .filter(({ setting }) => setting.level !== "error")
61
+ .map(({ rule, setting }) => looseRule(path, rule, setting));
62
+ }
63
+
64
+ export function biomeRecommendedFrom(recommended: () => Promise<readonly string[]>): Rule<"suppressions/biome-recommended", undefined> {
65
+ return defineRule({
66
+ check: ({ readText }) => check(readText, recommended),
67
+ description:
68
+ "Every rule the installed Biome recommends is at error in the shared preset, or the preset declares why not, so a Biome upgrade that adds or changes a recommended rule cannot slip in at Biome's default level.",
69
+ id: "suppressions/biome-recommended",
70
+ registrable: false,
71
+ });
72
+ }
73
+
74
+ export const biomeRecommended = biomeRecommendedFrom(recommendedRules);