@shivaedev/quality 0.2.0 → 0.3.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 (258) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/README.md +207 -35
  3. package/dist/baseline/compare.d.ts +3 -8
  4. package/dist/baseline/compare.d.ts.map +1 -1
  5. package/dist/baseline/compare.js +13 -23
  6. package/dist/baseline/compare.js.map +1 -1
  7. package/dist/baseline/format.d.ts +7 -5
  8. package/dist/baseline/format.d.ts.map +1 -1
  9. package/dist/baseline/format.js +31 -23
  10. package/dist/baseline/format.js.map +1 -1
  11. package/dist/baseline/guard.d.ts +23 -0
  12. package/dist/baseline/guard.d.ts.map +1 -0
  13. package/dist/baseline/guard.js +31 -0
  14. package/dist/baseline/guard.js.map +1 -0
  15. package/dist/baseline/legacy.d.ts +11 -0
  16. package/dist/baseline/legacy.d.ts.map +1 -0
  17. package/dist/baseline/legacy.js +33 -0
  18. package/dist/baseline/legacy.js.map +1 -0
  19. package/dist/baseline/rewrite.d.ts +3 -0
  20. package/dist/baseline/rewrite.d.ts.map +1 -0
  21. package/dist/baseline/rewrite.js +19 -0
  22. package/dist/baseline/rewrite.js.map +1 -0
  23. package/dist/baseline/update.d.ts +7 -6
  24. package/dist/baseline/update.d.ts.map +1 -1
  25. package/dist/baseline/update.js +28 -28
  26. package/dist/baseline/update.js.map +1 -1
  27. package/dist/cli/args.d.ts +14 -1
  28. package/dist/cli/args.d.ts.map +1 -1
  29. package/dist/cli/args.js +54 -29
  30. package/dist/cli/args.js.map +1 -1
  31. package/dist/cli/baseline.d.ts +3 -1
  32. package/dist/cli/baseline.d.ts.map +1 -1
  33. package/dist/cli/baseline.js +19 -10
  34. package/dist/cli/baseline.js.map +1 -1
  35. package/dist/cli/check.d.ts +5 -0
  36. package/dist/cli/check.d.ts.map +1 -0
  37. package/dist/cli/check.js +39 -0
  38. package/dist/cli/check.js.map +1 -0
  39. package/dist/cli/lint.js +1 -1
  40. package/dist/cli/lint.js.map +1 -1
  41. package/dist/cli/migrate.d.ts +4 -0
  42. package/dist/cli/migrate.d.ts.map +1 -0
  43. package/dist/cli/migrate.js +31 -0
  44. package/dist/cli/migrate.js.map +1 -0
  45. package/dist/cli/run-main.d.ts +2 -1
  46. package/dist/cli/run-main.d.ts.map +1 -1
  47. package/dist/cli/run-main.js +5 -2
  48. package/dist/cli/run-main.js.map +1 -1
  49. package/dist/cli/shrink.d.ts +6 -0
  50. package/dist/cli/shrink.d.ts.map +1 -0
  51. package/dist/cli/shrink.js +8 -0
  52. package/dist/cli/shrink.js.map +1 -0
  53. package/dist/cli.js +11 -2
  54. package/dist/cli.js.map +1 -1
  55. package/dist/config/decode.d.ts +1 -0
  56. package/dist/config/decode.d.ts.map +1 -1
  57. package/dist/config/decode.js +1 -0
  58. package/dist/config/decode.js.map +1 -1
  59. package/dist/config/file.d.ts +2 -0
  60. package/dist/config/file.d.ts.map +1 -0
  61. package/dist/config/file.js +2 -0
  62. package/dist/config/file.js.map +1 -0
  63. package/dist/config/load.d.ts +1 -1
  64. package/dist/config/load.d.ts.map +1 -1
  65. package/dist/config/load.js +3 -2
  66. package/dist/config/load.js.map +1 -1
  67. package/dist/config/resolve.d.ts +1 -0
  68. package/dist/config/resolve.d.ts.map +1 -1
  69. package/dist/config/resolve.js +6 -4
  70. package/dist/config/resolve.js.map +1 -1
  71. package/dist/config.d.ts +2 -7
  72. package/dist/config.d.ts.map +1 -1
  73. package/dist/config.js.map +1 -1
  74. package/dist/engine/baseline-file.d.ts +11 -0
  75. package/dist/engine/baseline-file.d.ts.map +1 -0
  76. package/dist/engine/baseline-file.js +18 -0
  77. package/dist/engine/baseline-file.js.map +1 -0
  78. package/dist/engine/evaluate.d.ts +2 -2
  79. package/dist/engine/evaluate.d.ts.map +1 -1
  80. package/dist/engine/evaluate.js +3 -3
  81. package/dist/engine/evaluate.js.map +1 -1
  82. package/dist/engine/session.d.ts +3 -5
  83. package/dist/engine/session.d.ts.map +1 -1
  84. package/dist/engine/session.js +12 -10
  85. package/dist/engine/session.js.map +1 -1
  86. package/dist/exceptions/registry.d.ts +1 -1
  87. package/dist/exceptions/registry.d.ts.map +1 -1
  88. package/dist/exceptions/registry.js +8 -3
  89. package/dist/exceptions/registry.js.map +1 -1
  90. package/dist/failure.d.ts +0 -2
  91. package/dist/failure.d.ts.map +1 -1
  92. package/dist/failure.js +0 -2
  93. package/dist/failure.js.map +1 -1
  94. package/dist/git/base.d.ts +9 -0
  95. package/dist/git/base.d.ts.map +1 -0
  96. package/dist/git/base.js +34 -0
  97. package/dist/git/base.js.map +1 -0
  98. package/dist/git/command.d.ts +12 -0
  99. package/dist/git/command.d.ts.map +1 -0
  100. package/dist/git/command.js +13 -0
  101. package/dist/git/command.js.map +1 -0
  102. package/dist/git/history.d.ts +10 -0
  103. package/dist/git/history.d.ts.map +1 -0
  104. package/dist/git/history.js +28 -0
  105. package/dist/git/history.js.map +1 -0
  106. package/dist/inventory/ignore-scope.d.ts.map +1 -1
  107. package/dist/inventory/ignore-scope.js +0 -1
  108. package/dist/inventory/ignore-scope.js.map +1 -1
  109. package/dist/inventory/walk.d.ts +0 -1
  110. package/dist/inventory/walk.d.ts.map +1 -1
  111. package/dist/inventory/walk.js +1 -1
  112. package/dist/inventory/walk.js.map +1 -1
  113. package/dist/report/guard.d.ts +9 -0
  114. package/dist/report/guard.d.ts.map +1 -0
  115. package/dist/report/guard.js +34 -0
  116. package/dist/report/guard.js.map +1 -0
  117. package/dist/report/render.js +1 -2
  118. package/dist/report/render.js.map +1 -1
  119. package/dist/report/stale.d.ts.map +1 -1
  120. package/dist/report/stale.js +4 -3
  121. package/dist/report/stale.js.map +1 -1
  122. package/dist/rule.d.ts +4 -7
  123. package/dist/rule.d.ts.map +1 -1
  124. package/dist/rule.js +1 -1
  125. package/dist/rule.js.map +1 -1
  126. package/dist/rules/built-in.d.ts +17 -0
  127. package/dist/rules/built-in.d.ts.map +1 -1
  128. package/dist/rules/built-in.js +21 -1
  129. package/dist/rules/built-in.js.map +1 -1
  130. package/dist/rules/comments/kinds.d.ts +9 -0
  131. package/dist/rules/comments/kinds.d.ts.map +1 -0
  132. package/dist/rules/comments/kinds.js +34 -0
  133. package/dist/rules/comments/kinds.js.map +1 -0
  134. package/dist/rules/comments/max-per-file.d.ts +5 -0
  135. package/dist/rules/comments/max-per-file.d.ts.map +1 -0
  136. package/dist/rules/comments/max-per-file.js +39 -0
  137. package/dist/rules/comments/max-per-file.js.map +1 -0
  138. package/dist/rules/comments/no-banner.d.ts +2 -0
  139. package/dist/rules/comments/no-banner.d.ts.map +1 -0
  140. package/dist/rules/comments/no-banner.js +9 -0
  141. package/dist/rules/comments/no-banner.js.map +1 -0
  142. package/dist/rules/comments/no-jsdoc.d.ts +4 -0
  143. package/dist/rules/comments/no-jsdoc.d.ts.map +1 -0
  144. package/dist/rules/comments/no-jsdoc.js +14 -0
  145. package/dist/rules/comments/no-jsdoc.js.map +1 -0
  146. package/dist/rules/comments/no-line-reference.d.ts +2 -0
  147. package/dist/rules/comments/no-line-reference.d.ts.map +1 -0
  148. package/dist/rules/comments/no-line-reference.js +9 -0
  149. package/dist/rules/comments/no-line-reference.js.map +1 -0
  150. package/dist/rules/comments/no-pr-reference.d.ts +2 -0
  151. package/dist/rules/comments/no-pr-reference.d.ts.map +1 -0
  152. package/dist/rules/comments/no-pr-reference.js +13 -0
  153. package/dist/rules/comments/no-pr-reference.js.map +1 -0
  154. package/dist/rules/comments/no-todo.d.ts +2 -0
  155. package/dist/rules/comments/no-todo.d.ts.map +1 -0
  156. package/dist/rules/comments/no-todo.js +8 -0
  157. package/dist/rules/comments/no-todo.js.map +1 -0
  158. package/dist/rules/comments/pattern-rule.d.ts +10 -0
  159. package/dist/rules/comments/pattern-rule.d.ts.map +1 -0
  160. package/dist/rules/comments/pattern-rule.js +16 -0
  161. package/dist/rules/comments/pattern-rule.js.map +1 -0
  162. package/dist/rules/comments/scan.d.ts +13 -0
  163. package/dist/rules/comments/scan.d.ts.map +1 -0
  164. package/dist/rules/comments/scan.js +62 -0
  165. package/dist/rules/comments/scan.js.map +1 -0
  166. package/dist/rules/max-lines.d.ts.map +1 -1
  167. package/dist/rules/max-lines.js +8 -1
  168. package/dist/rules/max-lines.js.map +1 -1
  169. package/dist/rules/suppressions/biome/configs.d.ts +14 -0
  170. package/dist/rules/suppressions/biome/configs.d.ts.map +1 -0
  171. package/dist/rules/suppressions/biome/configs.js +39 -0
  172. package/dist/rules/suppressions/biome/configs.js.map +1 -0
  173. package/dist/rules/suppressions/biome/json.d.ts +27 -0
  174. package/dist/rules/suppressions/biome/json.d.ts.map +1 -0
  175. package/dist/rules/suppressions/biome/json.js +37 -0
  176. package/dist/rules/suppressions/biome/json.js.map +1 -0
  177. package/dist/rules/suppressions/biome/weakenings.d.ts +9 -0
  178. package/dist/rules/suppressions/biome/weakenings.d.ts.map +1 -0
  179. package/dist/rules/suppressions/biome/weakenings.js +68 -0
  180. package/dist/rules/suppressions/biome/weakenings.js.map +1 -0
  181. package/dist/rules/suppressions/biome-overrides.d.ts +8 -0
  182. package/dist/rules/suppressions/biome-overrides.d.ts.map +1 -0
  183. package/dist/rules/suppressions/biome-overrides.js +47 -0
  184. package/dist/rules/suppressions/biome-overrides.js.map +1 -0
  185. package/dist/rules/suppressions/directives.d.ts +4 -0
  186. package/dist/rules/suppressions/directives.d.ts.map +1 -0
  187. package/dist/rules/suppressions/directives.js +19 -0
  188. package/dist/rules/suppressions/directives.js.map +1 -0
  189. package/dist/rules/suppressions/no-double-cast.d.ts +2 -0
  190. package/dist/rules/suppressions/no-double-cast.d.ts.map +1 -0
  191. package/dist/rules/suppressions/no-double-cast.js +38 -0
  192. package/dist/rules/suppressions/no-double-cast.js.map +1 -0
  193. package/dist/rules/suppressions/no-inline.d.ts +8 -0
  194. package/dist/rules/suppressions/no-inline.d.ts.map +1 -0
  195. package/dist/rules/suppressions/no-inline.js +48 -0
  196. package/dist/rules/suppressions/no-inline.js.map +1 -0
  197. package/dist/rules/suppressions/stylesheet-comments.d.ts +4 -0
  198. package/dist/rules/suppressions/stylesheet-comments.d.ts.map +1 -0
  199. package/dist/rules/suppressions/stylesheet-comments.js +20 -0
  200. package/dist/rules/suppressions/stylesheet-comments.js.map +1 -0
  201. package/dist/rules/syntax.d.ts +4 -0
  202. package/dist/rules/syntax.d.ts.map +1 -0
  203. package/dist/rules/syntax.js +12 -0
  204. package/dist/rules/syntax.js.map +1 -0
  205. package/package.json +4 -5
  206. package/src/baseline/compare.ts +15 -39
  207. package/src/baseline/format.ts +35 -32
  208. package/src/baseline/guard.ts +47 -0
  209. package/src/baseline/legacy.ts +46 -0
  210. package/src/baseline/rewrite.ts +24 -0
  211. package/src/baseline/update.ts +47 -30
  212. package/src/cli/args.ts +64 -30
  213. package/src/cli/baseline.ts +27 -13
  214. package/src/cli/check.ts +51 -0
  215. package/src/cli/lint.ts +1 -1
  216. package/src/cli/migrate.ts +36 -0
  217. package/src/cli/run-main.ts +8 -3
  218. package/src/cli/shrink.ts +31 -0
  219. package/src/cli.ts +11 -2
  220. package/src/config/decode.ts +1 -0
  221. package/src/config/file.ts +1 -0
  222. package/src/config/load.ts +4 -3
  223. package/src/config/resolve.ts +7 -4
  224. package/src/config.ts +3 -7
  225. package/src/engine/baseline-file.ts +27 -0
  226. package/src/engine/evaluate.ts +4 -2
  227. package/src/engine/session.ts +14 -12
  228. package/src/exceptions/registry.ts +9 -2
  229. package/src/failure.ts +0 -2
  230. package/src/git/base.ts +54 -0
  231. package/src/git/command.ts +31 -0
  232. package/src/git/history.ts +41 -0
  233. package/src/inventory/ignore-scope.ts +0 -1
  234. package/src/inventory/walk.ts +1 -1
  235. package/src/report/guard.ts +46 -0
  236. package/src/report/render.ts +2 -2
  237. package/src/report/stale.ts +11 -3
  238. package/src/rule.ts +5 -8
  239. package/src/rules/built-in.ts +21 -1
  240. package/src/rules/comments/kinds.ts +45 -0
  241. package/src/rules/comments/max-per-file.ts +46 -0
  242. package/src/rules/comments/no-banner.ts +10 -0
  243. package/src/rules/comments/no-jsdoc.ts +18 -0
  244. package/src/rules/comments/no-line-reference.ts +11 -0
  245. package/src/rules/comments/no-pr-reference.ts +13 -0
  246. package/src/rules/comments/no-todo.ts +8 -0
  247. package/src/rules/comments/pattern-rule.ts +28 -0
  248. package/src/rules/comments/scan.ts +80 -0
  249. package/src/rules/max-lines.ts +8 -1
  250. package/src/rules/suppressions/biome/configs.ts +50 -0
  251. package/src/rules/suppressions/biome/json.ts +55 -0
  252. package/src/rules/suppressions/biome/weakenings.ts +102 -0
  253. package/src/rules/suppressions/biome-overrides.ts +61 -0
  254. package/src/rules/suppressions/directives.ts +25 -0
  255. package/src/rules/suppressions/no-double-cast.ts +47 -0
  256. package/src/rules/suppressions/no-inline.ts +63 -0
  257. package/src/rules/suppressions/stylesheet-comments.ts +27 -0
  258. package/src/rules/syntax.ts +14 -0
@@ -1,7 +1,10 @@
1
+ import * as NodeChildProcessSpawner from "@effect/platform-node/NodeChildProcessSpawner";
1
2
  import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem";
3
+ import * as NodePath from "@effect/platform-node/NodePath";
2
4
  import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
3
- import { Cause, Console, Effect, Exit, type FileSystem, Runtime } from "effect";
5
+ import { Cause, Console, Effect, Exit, type FileSystem, Layer, Runtime } from "effect";
4
6
  import { SetupFailure } from "../failure.ts";
7
+ import type { Git } from "../git/command.ts";
5
8
 
6
9
  const INTERRUPTED = 130;
7
10
  const COULD_NOT_RUN = 2;
@@ -24,7 +27,9 @@ const teardown: Runtime.Teardown = (exit, onExit) => {
24
27
  return onExit(Cause.hasInterruptsOnly(exit.cause) ? INTERRUPTED : exitCodeOf(Cause.squash(exit.cause)));
25
28
  };
26
29
 
27
- export const runMain = <Value, Error>(program: Effect.Effect<Value, Error, FileSystem.FileSystem>): void => {
30
+ const services = NodeChildProcessSpawner.layer.pipe(Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)));
31
+
32
+ export const runMain = <Value, Error>(program: Effect.Effect<Value, Error, FileSystem.FileSystem | Git>): void => {
28
33
  const explained = Effect.tapCause(program, (cause) => (Cause.hasInterruptsOnly(cause) ? Effect.void : explain(cause)));
29
- NodeRuntime.runMain(Effect.provide(explained, NodeFileSystem.layer), { disableErrorReporting: true, teardown });
34
+ NodeRuntime.runMain(Effect.provide(explained, services), { disableErrorReporting: true, teardown });
30
35
  };
@@ -0,0 +1,31 @@
1
+ import { Console, Effect, type FileSystem } from "effect";
2
+ import type { Scope } from "../baseline/update.ts";
3
+ import type { SetupFailure } from "../failure.ts";
4
+ import { resolveBase } from "../git/base.ts";
5
+ import type { Git } from "../git/command.ts";
6
+ import { changesSince } from "../git/history.ts";
7
+ import { shrinkBaseline } from "./baseline.ts";
8
+
9
+ const followedMoves = (root: string, against: string | undefined): Effect.Effect<Scope, SetupFailure, Git> =>
10
+ resolveBase(root, against).pipe(
11
+ Effect.flatMap((base) => changesSince(root, [base.commit])),
12
+ Effect.map((changes): Scope => ({ moves: changes.moves })),
13
+ Effect.catchIf(
14
+ () => against === undefined,
15
+ (failure) => Effect.as(Console.error(`quality: moved files are not followed: ${failure.message}`), { moves: new Map() }),
16
+ ),
17
+ );
18
+
19
+ export const pruneBaseline = (
20
+ cwd: string,
21
+ config: string | undefined,
22
+ against: string | undefined,
23
+ ): Effect.Effect<void, SetupFailure, FileSystem.FileSystem | Git> =>
24
+ shrinkBaseline(cwd, config, (session) => followedMoves(session.config.root, against));
25
+
26
+ export const tightenBaseline = (
27
+ cwd: string,
28
+ config: string | undefined,
29
+ staged: boolean,
30
+ ): Effect.Effect<void, SetupFailure, FileSystem.FileSystem | Git> =>
31
+ shrinkBaseline(cwd, config, (session) => changesSince(session.config.root, staged ? ["--cached"] : ["HEAD"]));
package/src/cli.ts CHANGED
@@ -2,9 +2,12 @@
2
2
  import process from "node:process";
3
3
  import { Console, Effect } from "effect";
4
4
  import { parseCommand, USAGE } from "./cli/args.ts";
5
- import { pruneBaseline, writeBaseline } from "./cli/baseline.ts";
5
+ import { writeBaseline } from "./cli/baseline.ts";
6
+ import { checkBaseline } from "./cli/check.ts";
6
7
  import { lint } from "./cli/lint.ts";
8
+ import { migrateBaseline } from "./cli/migrate.ts";
7
9
  import { runMain } from "./cli/run-main.ts";
10
+ import { pruneBaseline, tightenBaseline } from "./cli/shrink.ts";
8
11
  import { SetupFailure } from "./failure.ts";
9
12
 
10
13
  const parsed = parseCommand(process.argv.slice(2));
@@ -22,7 +25,13 @@ const program = Effect.gen(function* () {
22
25
  case "BaselineWrite":
23
26
  return yield* writeBaseline(process.cwd(), command.config, command.rules);
24
27
  case "BaselinePrune":
25
- return yield* pruneBaseline(process.cwd(), command.config);
28
+ return yield* pruneBaseline(process.cwd(), command.config, command.against);
29
+ case "BaselineTighten":
30
+ return yield* tightenBaseline(process.cwd(), command.config, command.staged);
31
+ case "BaselineCheck":
32
+ return yield* checkBaseline(process.cwd(), command.config, command.against);
33
+ case "BaselineMigrate":
34
+ return yield* migrateBaseline(process.cwd(), command.config, command.from);
26
35
  }
27
36
  });
28
37
 
@@ -24,6 +24,7 @@ const ConfigInput = Schema.Struct({
24
24
  extensions: Schema.optionalKey(Schema.Array(Schema.String)),
25
25
  registry: Schema.optionalKey(Schema.String),
26
26
  baseline: Schema.optionalKey(Schema.String),
27
+ adopt: Schema.optionalKey(Schema.Array(Schema.String)),
27
28
  local: Schema.optionalKey(Schema.Array(LocalRule)),
28
29
  rules: Schema.optionalKey(Schema.Record(Schema.String, Setting)),
29
30
  });
@@ -0,0 +1 @@
1
+ export const CONFIG_FILE = "quality.config.ts";
@@ -5,10 +5,9 @@ import { validOrFail } from "../decoded.ts";
5
5
  import { SetupFailure } from "../failure.ts";
6
6
  import type { InventoryScope } from "../inventory/collect.ts";
7
7
  import { type ConfigInput, decodeConfig } from "./decode.ts";
8
+ import { CONFIG_FILE } from "./file.ts";
8
9
  import { type ResolvedRules, resolveRules } from "./resolve.ts";
9
10
 
10
- export const CONFIG_FILE = "quality.config.ts";
11
-
12
11
  const DEFAULT_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
13
12
 
14
13
  export interface ResolvedConfig extends ResolvedRules, InventoryScope {
@@ -16,6 +15,7 @@ export interface ResolvedConfig extends ResolvedRules, InventoryScope {
16
15
  readonly file: string;
17
16
  readonly registry: string;
18
17
  readonly baseline: string;
18
+ readonly adopt: ReadonlyArray<string>;
19
19
  }
20
20
 
21
21
  const messageOf = (cause: unknown): string => (cause instanceof Error ? cause.message : String(cause));
@@ -34,7 +34,8 @@ const importDefault = (file: string): Effect.Effect<unknown, SetupFailure> =>
34
34
 
35
35
  const resolved = (root: string, file: string, config: ConfigInput, rules: ResolvedRules): ResolvedConfig => ({
36
36
  ...rules,
37
- baseline: config.baseline ?? "quality/baseline.json",
37
+ adopt: config.adopt ?? [],
38
+ baseline: config.baseline ?? "quality/baseline.jsonl",
38
39
  exclude: config.exclude ?? [],
39
40
  extensions: config.extensions ?? DEFAULT_EXTENSIONS,
40
41
  file,
@@ -9,6 +9,7 @@ import type { ConfigInput } from "./decode.ts";
9
9
  export interface ResolvedRules {
10
10
  readonly active: ReadonlyArray<ActiveRule>;
11
11
  readonly levels: ReadonlyMap<string, Level>;
12
+ readonly unregistrable: ReadonlySet<string>;
12
13
  }
13
14
 
14
15
  type Setting = NonNullable<ConfigInput["rules"]>[string];
@@ -47,14 +48,16 @@ export const resolveRules = (config: ConfigInput): Effect.Effect<Decoded<Resolve
47
48
  const rules: ReadonlyArray<Rule> = [...builtInRules, ...(config.local ?? [])];
48
49
  const settings = config.rules ?? {};
49
50
  const known = new Set(rules.map((rule) => rule.id));
50
- const unknown = Object.keys(settings)
51
- .filter((id) => !known.has(id))
52
- .map((id) => `rules.${id}: no built-in or local rule has this id`);
51
+ const unknown = [...Object.keys(settings).map((id) => `rules.${id}`), ...(config.adopt ?? []).map((id) => `adopt.${id}`)]
52
+ .filter((path) => !known.has(path.slice(path.indexOf(".") + 1)))
53
+ .map((path) => `${path}: no built-in or local rule has this id`);
53
54
  const activated = yield* Effect.forEach(rules, (rule) => activate(rule, settings[rule.id]));
54
55
  const issues = [...duplicateIssues(rules), ...unknown, ...activated.flatMap((result) => (result._tag === "Invalid" ? result.issues : []))];
55
56
  if (issues.length > 0) {
56
57
  return { _tag: "Invalid", issues };
57
58
  }
58
59
  const active = activated.flatMap((result) => (result._tag === "Valid" && result.value !== undefined ? [result.value] : []));
59
- return { _tag: "Valid", value: { active, levels: new Map(rules.map((rule) => [rule.id, levelOf(settings[rule.id])])) } };
60
+ const levels = new Map(rules.map((rule) => [rule.id, levelOf(settings[rule.id])]));
61
+ const unregistrable = new Set(rules.filter((rule) => rule.registrable === false).map((rule) => rule.id));
62
+ return { _tag: "Valid", value: { active, levels, unregistrable } };
60
63
  });
package/src/config.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import type { Rule } from "./rule.ts";
2
2
  import type { builtInRules } from "./rules/built-in.ts";
3
3
 
4
- /** Every rule defaults to `error`. `warn` is for the transition to a rule and is meant to be temporary. */
5
4
  export type Level = "error" | "warn" | "off";
6
5
 
7
6
  export type RuleSetting<Input> = Level | { readonly level?: Level; readonly options?: Input };
@@ -14,18 +13,15 @@ export type RuleSettings<Rules extends ReadonlyArray<Rule>> = {
14
13
 
15
14
  export type BuiltInRules = typeof builtInRules;
16
15
 
16
+ type IdOf<Rules extends ReadonlyArray<Rule>> = Rules[number]["id"];
17
+
17
18
  export interface QualityConfig<Local extends ReadonlyArray<Rule> = ReadonlyArray<Rule>> {
18
- /** Directories or files to check, relative to the config file. Defaults to the whole repository. */
19
19
  readonly sources?: ReadonlyArray<string>;
20
- /** Paths never checked, in .gitignore syntax. Ignored files are always skipped. */
21
20
  readonly exclude?: ReadonlyArray<string>;
22
- /** Extensions of the source files rules read. Defaults to TypeScript and JavaScript modules. */
23
21
  readonly extensions?: ReadonlyArray<string>;
24
- /** Permanent exceptions, each with a reason. Defaults to `quality/registry.json`. */
25
22
  readonly registry?: string;
26
- /** Existing violations that may only shrink. Defaults to `quality/baseline.json`. */
27
23
  readonly baseline?: string;
28
- /** Repository rules, reported, registered and baselined like the built-in ones. */
24
+ readonly adopt?: ReadonlyArray<IdOf<readonly [...BuiltInRules, ...Local]>>;
29
25
  readonly local?: Local;
30
26
  readonly rules?: RuleSettings<readonly [...BuiltInRules, ...Local]>;
31
27
  }
@@ -0,0 +1,27 @@
1
+ import { join } from "node:path";
2
+ import { Effect, type FileSystem } from "effect";
3
+ import { type BaselineEntry, decodeBaseline } from "../baseline/format.ts";
4
+ import { LEGACY_BASELINE } from "../baseline/legacy.ts";
5
+ import type { ResolvedConfig } from "../config/load.ts";
6
+ import { validOrFail } from "../decoded.ts";
7
+ import { SetupFailure } from "../failure.ts";
8
+ import { readOptionalText } from "../inventory/filesystem.ts";
9
+
10
+ export interface BaselineFile {
11
+ readonly raw: string | undefined;
12
+ readonly entries: ReadonlyArray<BaselineEntry>;
13
+ }
14
+
15
+ export const readInput = (root: string, path: string): Effect.Effect<string | undefined, SetupFailure, FileSystem.FileSystem> =>
16
+ Effect.mapError(readOptionalText(join(root, path)), (failure) => new SetupFailure({ message: failure.message }));
17
+
18
+ export const readBaseline = (config: ResolvedConfig): Effect.Effect<BaselineFile, SetupFailure, FileSystem.FileSystem> =>
19
+ Effect.gen(function* () {
20
+ const raw = yield* readInput(config.root, config.baseline);
21
+ if (raw === undefined && config.baseline !== LEGACY_BASELINE && (yield* readInput(config.root, LEGACY_BASELINE)) !== undefined) {
22
+ return yield* new SetupFailure({
23
+ message: `${LEGACY_BASELINE} holds a baseline in the earlier JSON format. Run \`quality baseline migrate\` to move it to ${config.baseline}.`,
24
+ });
25
+ }
26
+ return { entries: yield* validOrFail(config.baseline, yield* Effect.promise(() => decodeBaseline(raw))), raw };
27
+ });
@@ -9,24 +9,26 @@ export interface Outcome {
9
9
  readonly warnings: ReadonlyArray<Violation>;
10
10
  readonly regressions: ReadonlyArray<Regression>;
11
11
  readonly staleBaseline: ReadonlyArray<StaleBaselineEntry>;
12
+ readonly looseBaseline: ReadonlyArray<StaleBaselineEntry>;
12
13
  readonly staleRegistry: ReadonlyArray<StaleRegistryEntry>;
13
14
  readonly baselined: number;
14
15
  readonly registered: number;
15
16
  }
16
17
 
17
- /** Registered exceptions apply first; the baseline then covers what is left, per rule and file. */
18
18
  export const evaluate = (
19
19
  violations: ReadonlyArray<Violation>,
20
20
  registry: ReadonlyArray<RegistryEntry>,
21
21
  baseline: ReadonlyArray<BaselineEntry>,
22
22
  levels: ReadonlyMap<string, Level>,
23
+ unregistrable: ReadonlySet<string>,
23
24
  ): Outcome => {
24
- const registered = applyRegistry(violations, registry, levels);
25
+ const registered = applyRegistry(violations, registry, levels, unregistrable);
25
26
  const baselined = applyBaseline(registered.kept, baseline, levels);
26
27
  const kept = [...baselined.kept].sort(byLocation);
27
28
  return {
28
29
  baselined: baselined.baselined,
29
30
  errors: kept.filter((violation) => violation.level === "error"),
31
+ looseBaseline: baselined.loose,
30
32
  regressions: baselined.regressions,
31
33
  registered: registered.registered,
32
34
  staleBaseline: baselined.stale,
@@ -1,12 +1,12 @@
1
- import { join, resolve } from "node:path";
1
+ import { resolve } from "node:path";
2
2
  import { Effect, type FileSystem } from "effect";
3
- import { type BaselineEntry, decodeBaseline } from "../baseline/format.ts";
4
3
  import { loadConfig, type ResolvedConfig } from "../config/load.ts";
5
4
  import { validOrFail } from "../decoded.ts";
6
5
  import { decodeRegistry, type RegistryEntry } from "../exceptions/registry.ts";
7
6
  import { SetupFailure } from "../failure.ts";
8
7
  import { collectInventory, type Inventory } from "../inventory/collect.ts";
9
8
  import { type FilesystemFailure, readOptionalText } from "../inventory/filesystem.ts";
9
+ import { type BaselineFile, readBaseline, readInput } from "./baseline-file.ts";
10
10
  import { runRules } from "./run-rules.ts";
11
11
  import type { Violation } from "./violation.ts";
12
12
 
@@ -15,26 +15,28 @@ export interface Session {
15
15
  readonly inventory: Inventory;
16
16
  readonly violations: ReadonlyArray<Violation>;
17
17
  readonly registry: ReadonlyArray<RegistryEntry>;
18
- readonly baseline: { readonly raw: string | undefined; readonly entries: ReadonlyArray<BaselineEntry> };
18
+ readonly baseline: BaselineFile;
19
19
  }
20
20
 
21
21
  const setup = <Value, Requirements>(
22
22
  effect: Effect.Effect<Value, FilesystemFailure, Requirements>,
23
23
  ): Effect.Effect<Value, SetupFailure, Requirements> => Effect.mapError(effect, (failure) => new SetupFailure({ message: failure.message }));
24
24
 
25
- const readInput = (root: string, path: string): Effect.Effect<string | undefined, SetupFailure, FileSystem.FileSystem> =>
26
- setup(readOptionalText(join(root, path)));
25
+ export const scan = (config: ResolvedConfig): Effect.Effect<Pick<Session, "inventory" | "violations">, SetupFailure, FileSystem.FileSystem> =>
26
+ Effect.gen(function* () {
27
+ const inventory = yield* setup(collectInventory(config.root, config));
28
+ const services = yield* Effect.context<FileSystem.FileSystem>();
29
+ const readText = (path: string): Promise<string | undefined> => Effect.runPromiseWith(services)(readOptionalText(resolve(config.root, path)));
30
+ const violations = yield* runRules(config.active, { files: inventory.files, readText, root: config.root, sources: inventory.sources });
31
+ return { inventory, violations };
32
+ });
27
33
 
28
34
  export const openSession = (cwd: string, configPath: string | undefined): Effect.Effect<Session, SetupFailure, FileSystem.FileSystem> =>
29
35
  Effect.gen(function* () {
30
36
  const config = yield* loadConfig(cwd, configPath);
31
- const inventory = yield* setup(collectInventory(config.root, config));
32
37
  const registryRaw = yield* readInput(config.root, config.registry);
33
38
  const registry = yield* validOrFail(config.registry, yield* Effect.promise(() => decodeRegistry(registryRaw)));
34
- const baselineRaw = yield* readInput(config.root, config.baseline);
35
- const baseline = yield* validOrFail(config.baseline, yield* Effect.promise(() => decodeBaseline(baselineRaw)));
36
- const services = yield* Effect.context<FileSystem.FileSystem>();
37
- const readText = (path: string): Promise<string | undefined> => Effect.runPromiseWith(services)(readOptionalText(resolve(config.root, path)));
38
- const violations = yield* runRules(config.active, { files: inventory.files, readText, root: config.root, sources: inventory.sources });
39
- return { baseline: { entries: baseline, raw: baselineRaw }, config, inventory, registry, violations };
39
+ const baseline = yield* readBaseline(config);
40
+ const { inventory, violations } = yield* scan(config);
41
+ return { baseline, config, inventory, registry, violations };
40
42
  });
@@ -33,6 +33,8 @@ export interface RegistryCheck {
33
33
  const covers = (entry: RegistryEntry, violation: Violation): boolean =>
34
34
  entry.rule === violation.rule && entry.file === violation.file && (entry.subject === undefined || entry.subject === violation.subject);
35
35
 
36
+ const UNREGISTRABLE = "names a rule that takes no exceptions. Fix the code, or baseline the violations while the repository adopts the rule";
37
+
36
38
  const coveringEntry = (entries: ReadonlyArray<RegistryEntry>, violation: Violation): RegistryEntry | undefined => {
37
39
  const matching = entries.filter((entry) => covers(entry, violation));
38
40
  return matching.find((entry) => entry.subject !== undefined) ?? matching[0];
@@ -42,10 +44,12 @@ export const applyRegistry = (
42
44
  violations: ReadonlyArray<Violation>,
43
45
  entries: ReadonlyArray<RegistryEntry>,
44
46
  levels: ReadonlyMap<string, Level>,
47
+ unregistrable: ReadonlySet<string>,
45
48
  ): RegistryCheck => {
49
+ const usable = entries.filter((entry) => !unregistrable.has(entry.rule));
46
50
  const used = new Set<RegistryEntry>();
47
51
  const kept = violations.filter((violation) => {
48
- const entry = coveringEntry(entries, violation);
52
+ const entry = coveringEntry(usable, violation);
49
53
  if (entry !== undefined) {
50
54
  used.add(entry);
51
55
  }
@@ -53,6 +57,9 @@ export const applyRegistry = (
53
57
  });
54
58
  const stale = entries
55
59
  .filter((entry) => !used.has(entry))
56
- .map((entry) => ({ entry, problem: unusedEntryProblem(levels, entry.rule, "matches no violation") }));
60
+ .map((entry) => ({
61
+ entry,
62
+ problem: unregistrable.has(entry.rule) ? UNREGISTRABLE : unusedEntryProblem(levels, entry.rule, "matches no violation"),
63
+ }));
57
64
  return { kept, registered: violations.length - kept.length, stale };
58
65
  };
package/src/failure.ts CHANGED
@@ -1,12 +1,10 @@
1
1
  import { Data, Runtime } from "effect";
2
2
 
3
- /** The check could not run: invalid configuration, unreadable input or a crashed rule. Exits 2. */
4
3
  export class SetupFailure extends Data.TaggedError("SetupFailure")<{ readonly message: string }> {
5
4
  override readonly [Runtime.errorReported] = false;
6
5
  override readonly [Runtime.errorExitCode] = 2;
7
6
  }
8
7
 
9
- /** The check ran and the repository fails the gate. The report is already printed. Exits 1. */
10
8
  export class GateFailed extends Data.TaggedError("GateFailed") {
11
9
  override readonly [Runtime.errorReported] = false;
12
10
  override readonly [Runtime.errorExitCode] = 1;
@@ -0,0 +1,54 @@
1
+ import { Effect } from "effect";
2
+ import { SetupFailure } from "../failure.ts";
3
+ import { type Git, git } from "./command.ts";
4
+
5
+ export interface Base {
6
+ readonly ref: string;
7
+ readonly commit: string;
8
+ }
9
+
10
+ const FALLBACK_BRANCHES = ["origin/main", "origin/master"];
11
+
12
+ const fail = (message: string) => Effect.fail(new SetupFailure({ message }));
13
+
14
+ const isCommit = (root: string, ref: string): Effect.Effect<boolean, SetupFailure, Git> =>
15
+ Effect.map(git(root, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]), (result) => result.code === 0);
16
+
17
+ const defaultBranch = (root: string): Effect.Effect<string, SetupFailure, Git> =>
18
+ Effect.gen(function* () {
19
+ const head = yield* git(root, ["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"]);
20
+ if (head.code === 0) {
21
+ return head.stdout.trim();
22
+ }
23
+ for (const candidate of FALLBACK_BRANCHES) {
24
+ if (yield* isCommit(root, candidate)) {
25
+ return candidate;
26
+ }
27
+ }
28
+ return yield* fail(
29
+ "cannot find the default branch: origin/HEAD, origin/main and origin/master are missing. Pass --against <ref>, or run `git remote set-head origin --auto`.",
30
+ );
31
+ });
32
+
33
+ const missingMergeBase = (root: string, ref: string): Effect.Effect<never, SetupFailure, Git> =>
34
+ Effect.flatMap(git(root, ["rev-parse", "--is-shallow-repository"]), (shallow) =>
35
+ fail(
36
+ shallow.stdout.trim() === "true"
37
+ ? `the clone is shallow and does not reach the merge base of HEAD and ${ref}. Fetch the history first: \`git fetch --unshallow\`, or fetch-depth: 0 in actions/checkout.`
38
+ : `HEAD and ${ref} share no history.`,
39
+ ),
40
+ );
41
+
42
+ export const resolveBase = (root: string, against: string | undefined): Effect.Effect<Base, SetupFailure, Git> =>
43
+ Effect.gen(function* () {
44
+ const inside = yield* git(root, ["rev-parse", "--is-inside-work-tree"]);
45
+ if (inside.code !== 0 || inside.stdout.trim() !== "true") {
46
+ return yield* fail(`${root} is not in a git work tree; the baseline is compared with its version in git history.`);
47
+ }
48
+ const ref = against ?? (yield* defaultBranch(root));
49
+ if (!(yield* isCommit(root, ref))) {
50
+ return yield* fail(`${ref} names no commit in this clone. Fetch it first, or pass another --against <ref>.`);
51
+ }
52
+ const merged = yield* git(root, ["merge-base", "HEAD", ref]);
53
+ return merged.code === 0 ? { commit: merged.stdout.trim(), ref } : yield* missingMergeBase(root, ref);
54
+ });
@@ -0,0 +1,31 @@
1
+ import { Effect, Stream } from "effect";
2
+ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
3
+ import { SetupFailure } from "../failure.ts";
4
+
5
+ export interface GitResult {
6
+ readonly code: number;
7
+ readonly stdout: string;
8
+ readonly stderr: string;
9
+ }
10
+
11
+ export type Git = ChildProcessSpawner.ChildProcessSpawner;
12
+
13
+ export const git = (cwd: string, args: ReadonlyArray<string>): Effect.Effect<GitResult, SetupFailure, Git> =>
14
+ Effect.scoped(
15
+ Effect.gen(function* () {
16
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
17
+ const child = yield* spawner.spawn(ChildProcess.make("git", [...args], { cwd, stdin: "ignore" }));
18
+ const [stdout, stderr, code] = yield* Effect.all(
19
+ [Stream.mkString(Stream.decodeText(child.stdout)), Stream.mkString(Stream.decodeText(child.stderr)), child.exitCode],
20
+ { concurrency: "unbounded" },
21
+ );
22
+ return { code: Number(code), stderr, stdout };
23
+ }),
24
+ ).pipe(Effect.mapError((error) => new SetupFailure({ message: `cannot run git: ${error.message}` })));
25
+
26
+ export const gitOrFail = (cwd: string, args: ReadonlyArray<string>): Effect.Effect<string, SetupFailure, Git> =>
27
+ Effect.flatMap(git(cwd, args), (result) =>
28
+ result.code === 0
29
+ ? Effect.succeed(result.stdout)
30
+ : Effect.fail(new SetupFailure({ message: `git ${args[0]} failed: ${result.stderr.trim() || `exit code ${result.code}`}` })),
31
+ );
@@ -0,0 +1,41 @@
1
+ import { Effect } from "effect";
2
+ import type { SetupFailure } from "../failure.ts";
3
+ import { type Git, git, gitOrFail } from "./command.ts";
4
+
5
+ export interface Changes {
6
+ readonly files: ReadonlySet<string>;
7
+ readonly moves: ReadonlyMap<string, string>;
8
+ }
9
+
10
+ export const readAt = (root: string, commit: string, path: string): Effect.Effect<string | undefined, SetupFailure, Git> =>
11
+ Effect.gen(function* () {
12
+ const object = `${commit}:./${path}`;
13
+ const present = yield* git(root, ["cat-file", "-e", object]);
14
+ return present.code === 0 ? yield* gitOrFail(root, ["cat-file", "blob", object]) : undefined;
15
+ });
16
+
17
+ const parse = (output: string): Changes => {
18
+ const fields = output.split("\0");
19
+ const files = new Set<string>();
20
+ const moves = new Map<string, string>();
21
+ for (let index = 0; index < fields.length - 1; ) {
22
+ const status = fields[index] ?? "";
23
+ const paths = fields.slice(index + 1, index + (/^[RC]/.test(status) ? 3 : 2));
24
+ for (const path of paths) {
25
+ files.add(path);
26
+ }
27
+ const [from, to] = paths;
28
+ if (status.startsWith("R") && from !== undefined && to !== undefined) {
29
+ moves.set(from, to);
30
+ }
31
+ index += paths.length + 1;
32
+ }
33
+ return { files, moves };
34
+ };
35
+
36
+ // Paths are relative to the root, and only tracked files count: a move is seen once both of its sides are staged.
37
+ export const changesSince = (root: string, revision: ReadonlyArray<string>): Effect.Effect<Changes, SetupFailure, Git> =>
38
+ Effect.map(
39
+ gitOrFail(root, ["diff", "--no-color", "--no-ext-diff", "--find-renames", "--name-status", "-z", "--relative", ...revision, "--"]),
40
+ parse,
41
+ );
@@ -39,6 +39,5 @@ const matcherVerdict = (matcher: IgnoreMatcher, boundary: string | undefined, pa
39
39
  return result.unignored ? "kept" : "unknown";
40
40
  };
41
41
 
42
- // The deepest ignore file with an opinion wins.
43
42
  export const verdictFor = (scope: IgnoreScope, path: string, directory: boolean): Verdict =>
44
43
  scope.matchers.map((matcher) => matcherVerdict(matcher, scope.boundary, path, directory)).findLast((verdict) => verdict !== "unknown") ?? "unknown";
@@ -13,7 +13,7 @@ export const ignoreScopeAt = (
13
13
  contents === undefined || contents === "" ? inherited : withIgnoreFile(inherited, dir, contents),
14
14
  );
15
15
 
16
- /** The ignore files of every directory between the root and a walked directory, which the walk itself does not visit. */
16
+ // The walk itself never visits the directories between the root and a source, so their ignore files are read here.
17
17
  export const scopeAbove = (root: string, dir: string): Effect.Effect<IgnoreScope, FilesystemFailure, FileSystem.FileSystem> => {
18
18
  const parts = relative(root, dir)
19
19
  .split(sep)
@@ -0,0 +1,46 @@
1
+ import type { GuardProblem, GuardResult } from "../baseline/guard.ts";
2
+ import type { Base } from "../git/base.ts";
3
+ import { plural } from "./plural.ts";
4
+
5
+ export interface GuardContext {
6
+ readonly baseline: string;
7
+ readonly config: string;
8
+ readonly base: Base;
9
+ }
10
+
11
+ const problemLine = (problem: GuardProblem, context: GuardContext): string => {
12
+ switch (problem._tag) {
13
+ case "Added":
14
+ return `${problem.entry.rule} ${problem.entry.file} is new, at ${problem.entry.count}.`;
15
+ case "Raised":
16
+ return `${problem.entry.rule} ${problem.entry.file} rose to ${problem.entry.count} from ${problem.base.count}.`;
17
+ case "Unadopted":
18
+ return `${problem.rule} is newly baselined in ${plural(problem.entries, "file")} without being named under \`adopt\` in ${context.config}.`;
19
+ case "NothingAdopted":
20
+ return `\`adopt\` in ${context.config} names ${problem.rule}, which has nothing baselined. Remove it.`;
21
+ }
22
+ };
23
+
24
+ const against = (base: Base): string => `${base.ref} (merge base ${base.commit.slice(0, 12)})`;
25
+
26
+ const held = (result: GuardResult): string =>
27
+ [
28
+ ...(result.moved > 0 ? [`${plural(result.moved, "entry", "entries")} followed moved files`] : []),
29
+ ...(result.adopted.length > 0 ? [`adopted ${result.adopted.join(", ")}`] : []),
30
+ ]
31
+ .map((part) => `; ${part}`)
32
+ .join("");
33
+
34
+ export const renderGuard = (result: GuardResult, context: GuardContext): string => {
35
+ if (result.problems.length === 0) {
36
+ return `quality: ${context.baseline} holds against ${against(context.base)}${held(result)}.`;
37
+ }
38
+ return [
39
+ [
40
+ `error ${context.baseline} grew against ${against(context.base)}`,
41
+ ` The baseline only shrinks. Fix new violations instead of baselining them; a rule enters the baseline for the first time only while \`adopt\` names it.`,
42
+ ...result.problems.map((problem) => ` ${problemLine(problem, context)}`),
43
+ ].join("\n"),
44
+ `quality: baseline check failed with ${plural(result.problems.length, "problem")}.`,
45
+ ].join("\n\n");
46
+ };
@@ -1,4 +1,4 @@
1
- import { describeEntry, describeTally, type Regression } from "../baseline/compare.ts";
1
+ import type { Regression } from "../baseline/compare.ts";
2
2
  import { type Outcome, passes } from "../engine/evaluate.ts";
3
3
  import { groupBy, keyOf, type Violation } from "../engine/violation.ts";
4
4
  import { plural } from "./plural.ts";
@@ -19,7 +19,7 @@ const LISTED_FILES = 5;
19
19
  const locate = (violation: Violation): string => (violation.line === undefined ? violation.file : `${violation.file}:${violation.line}`);
20
20
 
21
21
  const regressionNote = (regression: Regression): string =>
22
- ` ${regression.entry.file} is over its baseline: ${describeTally(regression.tally)} against ${describeEntry(regression.entry)} baselined.`;
22
+ ` ${regression.entry.file} is over its baseline: ${regression.count} against ${regression.entry.count} baselined.`;
23
23
 
24
24
  const heading = (label: string, rule: string, violations: ReadonlyArray<Violation>, context: ReportContext): ReadonlyArray<string> => {
25
25
  const description = context.descriptions.get(rule);
@@ -8,8 +8,8 @@ interface StaleFiles {
8
8
  readonly baseline: string;
9
9
  }
10
10
 
11
- const section = (file: string, guidance: string, lines: ReadonlyArray<string>): ReadonlyArray<string> =>
12
- lines.length === 0 ? [] : [[`error ${file}: ${plural(lines.length, "stale entry", "stale entries")}`, ` ${guidance}`, ...lines].join("\n")];
11
+ const section = (label: string, file: string, guidance: string, lines: ReadonlyArray<string>): ReadonlyArray<string> =>
12
+ lines.length === 0 ? [] : [[`${label} ${file}: ${plural(lines.length, "stale entry", "stale entries")}`, ` ${guidance}`, ...lines].join("\n")];
13
13
 
14
14
  const baselineLine = ({ entry, problem }: StaleBaselineEntry): string => ` ${entry.rule} ${entry.file} ${problem}.`;
15
15
 
@@ -18,11 +18,19 @@ const registryLine = ({ entry, problem }: StaleRegistryEntry): string =>
18
18
 
19
19
  export const staleSections = (outcome: Outcome, files: StaleFiles): ReadonlyArray<string> => [
20
20
  ...section(
21
+ "error",
21
22
  files.baseline,
22
- "The baseline only shrinks. Run `quality baseline prune` to drop fixed debt; prune never adds or raises an entry.",
23
+ "An entry for a rule that is off or unknown covers nothing. Run `quality baseline prune` to drop it.",
23
24
  outcome.staleBaseline.map(baselineLine),
24
25
  ),
25
26
  ...section(
27
+ "note",
28
+ files.baseline,
29
+ "These pass, and `quality baseline tighten` lowers them when their files change. `quality baseline prune` lowers them all.",
30
+ outcome.looseBaseline.map(baselineLine),
31
+ ),
32
+ ...section(
33
+ "error",
26
34
  files.registry,
27
35
  "Every registered exception must still apply. Remove the entries whose exception is gone.",
28
36
  outcome.staleRegistry.map(registryLine),
package/src/rule.ts CHANGED
@@ -4,10 +4,9 @@ export interface Finding {
4
4
  readonly file: string;
5
5
  readonly line?: number | undefined;
6
6
  readonly message: string;
7
- /** Tells apart exceptions of one rule in one file, such as which Biome rule an override turns off. */
8
7
  readonly subject?: string | undefined;
9
- /** A size where larger is worse, such as a file's line count. A baselined file may not grow past it. */
10
- readonly measure?: number | undefined;
8
+ readonly count?: number | undefined;
9
+ readonly threshold?: number | undefined;
11
10
  }
12
11
 
13
12
  export type Findings = ReadonlyArray<Finding>;
@@ -36,6 +35,7 @@ export type Configured =
36
35
  export interface Rule<Id extends string = string, Input = unknown> {
37
36
  readonly id: Id;
38
37
  readonly description: string;
38
+ readonly registrable?: boolean;
39
39
  // Method syntax keeps rules with different options in one list; configure validates its input at runtime.
40
40
  configure(options: Input | undefined): Promise<Configured>;
41
41
  }
@@ -43,6 +43,7 @@ export interface Rule<Id extends string = string, Input = unknown> {
43
43
  interface RuleDefinition<Id extends string, Options> {
44
44
  readonly id: Id;
45
45
  readonly description: string;
46
+ readonly registrable?: false;
46
47
  readonly check: (context: RuleContext<Options>) => Findings | PromiseLike<Findings>;
47
48
  }
48
49
 
@@ -73,10 +74,6 @@ const configureWith =
73
74
  return { _tag: "Ready", check: async (inputs) => check({ ...inputs, options: value }) };
74
75
  };
75
76
 
76
- /**
77
- * Defines a rule. Options are an object validated by a Standard Schema; a rule the config gives no options validates `{}`,
78
- * so give each option a default or make it optional.
79
- */
80
77
  export function defineRule<const Id extends string, Input, Options>(definition: RuleDefinitionWithOptions<Id, Input, Options>): Rule<Id, Input>;
81
78
  export function defineRule<const Id extends string>(definition: RuleDefinitionWithoutOptions<Id>): Rule<Id, undefined>;
82
79
  export function defineRule<const Id extends string, Input, Options>(
@@ -86,5 +83,5 @@ export function defineRule<const Id extends string, Input, Options>(
86
83
  definition.options === undefined
87
84
  ? configureWith(noOptions, definition.check, undefined)
88
85
  : configureWith(definition.options, definition.check, {});
89
- return { configure, description: definition.description, id: definition.id };
86
+ return { configure, description: definition.description, id: definition.id, registrable: definition.registrable ?? true };
90
87
  }