@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,53 +1,56 @@
1
1
  import { Schema } from "effect";
2
2
  import { type Decoded, decodeWith } from "../decoded.ts";
3
+ import { keyOf } from "../engine/violation.ts";
3
4
 
4
- const Entry = Schema.Struct({
5
+ const Line = Schema.Struct({
6
+ path: Schema.NonEmptyString,
7
+ rule: Schema.NonEmptyString,
5
8
  count: Schema.Int.check(Schema.isGreaterThan(0)),
6
- measure: Schema.optionalKey(Schema.Finite),
7
9
  });
8
10
 
9
- const BaselineFile = Schema.Record(Schema.String, Schema.Record(Schema.String, Entry));
10
-
11
- const standard = Schema.toStandardSchemaV1(Schema.fromJsonString(BaselineFile), { parseOptions: { errors: "all", onExcessProperty: "error" } });
11
+ const standard = Schema.toStandardSchemaV1(Schema.fromJsonString(Line), { parseOptions: { errors: "all", onExcessProperty: "error" } });
12
12
 
13
13
  export interface BaselineEntry {
14
14
  readonly rule: string;
15
15
  readonly file: string;
16
16
  readonly count: number;
17
- readonly measure?: number | undefined;
18
17
  }
19
18
 
20
- type Stored = typeof Entry.Type;
21
-
22
19
  const codepoints = (left: string, right: string): number => (left < right ? -1 : Number(left > right));
23
20
 
24
- export const byRuleAndFile = (left: BaselineEntry, right: BaselineEntry): number =>
25
- codepoints(left.rule, right.rule) || codepoints(left.file, right.file);
21
+ export const byPathAndRule = (left: BaselineEntry, right: BaselineEntry): number =>
22
+ codepoints(left.file, right.file) || codepoints(left.rule, right.rule);
23
+
24
+ export const linesOf = (raw: string | undefined): ReadonlyArray<{ readonly line: number; readonly text: string }> =>
25
+ (raw ?? "")
26
+ .split("\n")
27
+ .map((text, index) => ({ line: index + 1, text }))
28
+ .filter((row) => row.text.trim() !== "");
26
29
 
27
30
  export const decodeBaseline = async (raw: string | undefined): Promise<Decoded<ReadonlyArray<BaselineEntry>>> => {
28
- if (raw === undefined) {
29
- return { _tag: "Valid", value: [] };
30
- }
31
- const decoded = await decodeWith(standard, raw);
32
- if (decoded._tag === "Invalid") {
33
- return decoded;
31
+ const issues: string[] = [];
32
+ const entries: BaselineEntry[] = [];
33
+ const seen = new Set<string>();
34
+ for (const { line, text } of linesOf(raw)) {
35
+ const decoded = await decodeWith(standard, text);
36
+ if (decoded._tag === "Invalid") {
37
+ issues.push(...decoded.issues.map((issue) => `line ${line}: ${issue}`));
38
+ continue;
39
+ }
40
+ const { path, ...stored } = decoded.value;
41
+ if (seen.has(keyOf(stored.rule, path))) {
42
+ issues.push(`line ${line}: repeats the entry for ${stored.rule} in ${path}`);
43
+ }
44
+ seen.add(keyOf(stored.rule, path));
45
+ entries.push({ ...stored, file: path });
34
46
  }
35
- const entries = Object.entries(decoded.value).flatMap(([rule, files]) =>
36
- Object.entries(files).map(([file, stored]) => ({ ...stored, file, rule })),
37
- );
38
- return { _tag: "Valid", value: entries };
47
+ return issues.length === 0 ? { _tag: "Valid", value: entries } : { _tag: "Invalid", issues };
39
48
  };
40
49
 
41
- const stored = (entry: BaselineEntry): Stored =>
42
- entry.measure === undefined ? { count: entry.count } : { count: entry.count, measure: entry.measure };
43
-
44
- export const encodeBaseline = (entries: ReadonlyArray<BaselineEntry>, indent: string): string => {
45
- const rules: Record<string, Record<string, Stored>> = {};
46
- for (const entry of [...entries].sort(byRuleAndFile)) {
47
- rules[entry.rule] = { ...rules[entry.rule], [entry.file]: stored(entry) };
48
- }
49
- return `${JSON.stringify(rules, null, indent)}\n`;
50
- };
50
+ export const encodeEntry = (entry: BaselineEntry): string => JSON.stringify({ path: entry.file, rule: entry.rule, count: entry.count });
51
51
 
52
- /** Keeps the indentation of the file being rewritten, so the repository formatter leaves it alone. */
53
- export const indentOf = (raw: string | undefined): string => /^([ \t]+)\S/m.exec(raw ?? "")?.[1] ?? "\t";
52
+ export const encodeBaseline = (entries: ReadonlyArray<BaselineEntry>): string =>
53
+ [...entries]
54
+ .sort(byPathAndRule)
55
+ .map((entry) => `${encodeEntry(entry)}\n`)
56
+ .join("");
@@ -0,0 +1,47 @@
1
+ import { groupBy, keyOf } from "../engine/violation.ts";
2
+ import type { BaselineEntry } from "./format.ts";
3
+
4
+ export type GuardProblem =
5
+ | { readonly _tag: "Added"; readonly entry: BaselineEntry }
6
+ | { readonly _tag: "Raised"; readonly entry: BaselineEntry; readonly base: BaselineEntry }
7
+ | { readonly _tag: "Unadopted"; readonly rule: string; readonly entries: number }
8
+ | { readonly _tag: "NothingAdopted"; readonly rule: string };
9
+
10
+ export interface GuardResult {
11
+ readonly problems: ReadonlyArray<GuardProblem>;
12
+ readonly moved: number;
13
+ readonly adopted: ReadonlyArray<string>;
14
+ }
15
+
16
+ export const guardBaseline = (
17
+ base: ReadonlyArray<BaselineEntry>,
18
+ working: ReadonlyArray<BaselineEntry>,
19
+ renames: ReadonlyMap<string, string>,
20
+ adopt: ReadonlyArray<string>,
21
+ ): GuardResult => {
22
+ const before = new Map(base.map((entry) => [keyOf(entry.rule, entry.file), entry]));
23
+ const covered = new Set(base.map((entry) => entry.rule));
24
+ const byRule = groupBy(working, (entry) => entry.rule);
25
+ const problems: GuardProblem[] = [];
26
+ let moved = 0;
27
+ for (const entry of working.filter((candidate) => covered.has(candidate.rule))) {
28
+ const exact = before.get(keyOf(entry.rule, entry.file));
29
+ const origin = renames.get(entry.file);
30
+ const matched = exact ?? (origin === undefined ? undefined : before.get(keyOf(entry.rule, origin)));
31
+ if (matched === undefined) {
32
+ problems.push({ _tag: "Added", entry });
33
+ } else if (entry.count > matched.count) {
34
+ problems.push({ _tag: "Raised", base: matched, entry });
35
+ } else if (exact === undefined) {
36
+ moved += 1;
37
+ }
38
+ }
39
+ const fresh = [...byRule.entries()].filter(([rule]) => !covered.has(rule));
40
+ for (const [rule, entries] of fresh.filter(([rule]) => !adopt.includes(rule))) {
41
+ problems.push({ _tag: "Unadopted", entries: entries.length, rule });
42
+ }
43
+ for (const rule of adopt.filter((candidate) => !byRule.has(candidate))) {
44
+ problems.push({ _tag: "NothingAdopted", rule });
45
+ }
46
+ return { adopted: fresh.map(([rule]) => rule).filter((rule) => adopt.includes(rule)), moved, problems };
47
+ };
@@ -0,0 +1,46 @@
1
+ import { Schema } from "effect";
2
+ import { type Decoded, decodeWith } from "../decoded.ts";
3
+ import { groupBy, keyOf, type Violation } from "../engine/violation.ts";
4
+ import { countOf } from "./compare.ts";
5
+ import type { BaselineEntry } from "./format.ts";
6
+
7
+ export const LEGACY_BASELINE = "quality/baseline.json";
8
+
9
+ const Entry = Schema.Struct({
10
+ count: Schema.Int.check(Schema.isGreaterThan(0)),
11
+ measure: Schema.optionalKey(Schema.Finite),
12
+ });
13
+
14
+ const LegacyFile = Schema.Record(Schema.String, Schema.Record(Schema.String, Entry));
15
+
16
+ const standard = Schema.toStandardSchemaV1(Schema.fromJsonString(LegacyFile), { parseOptions: { errors: "all", onExcessProperty: "error" } });
17
+
18
+ export interface LegacyEntry extends BaselineEntry {
19
+ readonly measure?: number | undefined;
20
+ }
21
+
22
+ export const decodeLegacyBaseline = async (raw: string): Promise<Decoded<ReadonlyArray<LegacyEntry>>> => {
23
+ const decoded = await decodeWith(standard, raw);
24
+ if (decoded._tag === "Invalid") {
25
+ return decoded;
26
+ }
27
+ const entries = Object.entries(decoded.value).flatMap(([rule, files]) =>
28
+ Object.entries(files).map(([file, stored]) => ({ ...stored, file, rule })),
29
+ );
30
+ return { _tag: "Valid", value: entries };
31
+ };
32
+
33
+ export const measured = (legacy: ReadonlyArray<LegacyEntry>): boolean => legacy.some((entry) => entry.measure !== undefined);
34
+
35
+ export const convertLegacy = (legacy: ReadonlyArray<LegacyEntry>, violations: ReadonlyArray<Violation>): ReadonlyArray<BaselineEntry> => {
36
+ const current = groupBy(violations, (violation) => keyOf(violation.rule, violation.file));
37
+ return legacy.flatMap(({ count, file, measure, rule }) => {
38
+ if (measure === undefined) {
39
+ return [{ count, file, rule }];
40
+ }
41
+ const group = current.get(keyOf(rule, file)) ?? [];
42
+ const threshold = group.find((violation) => violation.threshold !== undefined)?.threshold;
43
+ const converted = threshold === undefined ? countOf(group) : Math.ceil(measure - threshold);
44
+ return converted > 0 ? [{ count: converted, file, rule }] : [];
45
+ });
46
+ };
@@ -0,0 +1,24 @@
1
+ import { keyOf } from "../engine/violation.ts";
2
+ import { type BaselineEntry, byPathAndRule, encodeEntry, linesOf } from "./format.ts";
3
+
4
+ interface Row {
5
+ readonly entry: BaselineEntry;
6
+ readonly text: string;
7
+ }
8
+
9
+ // An untouched entry keeps its exact line and a new one goes before the first line that sorts after it, so two branches conflict only on the same or neighbouring entries.
10
+ export const rewriteBaseline = (raw: string | undefined, before: ReadonlyArray<BaselineEntry>, after: ReadonlyArray<BaselineEntry>): string => {
11
+ const wanted = new Map(after.map((entry) => [keyOf(entry.rule, entry.file), entry]));
12
+ const lines = linesOf(raw);
13
+ const rows: Row[] = before.flatMap((old, index) => {
14
+ const entry = wanted.get(keyOf(old.rule, old.file));
15
+ const line = lines[index];
16
+ wanted.delete(keyOf(old.rule, old.file));
17
+ return entry === undefined ? [] : [{ entry, text: line !== undefined && old.count === entry.count ? line.text : encodeEntry(entry) }];
18
+ });
19
+ for (const entry of [...wanted.values()].sort(byPathAndRule)) {
20
+ const at = rows.findIndex((row) => byPathAndRule(row.entry, entry) > 0);
21
+ rows.splice(at === -1 ? rows.length : at, 0, { entry, text: encodeEntry(entry) });
22
+ }
23
+ return rows.map((row) => `${row.text}\n`).join("");
24
+ };
@@ -1,14 +1,15 @@
1
1
  import type { Level } from "../config.ts";
2
2
  import { groupBy, keyOf, type Violation } from "../engine/violation.ts";
3
- import { tallyOf } from "./compare.ts";
3
+ import { countOf } from "./compare.ts";
4
4
  import type { BaselineEntry } from "./format.ts";
5
5
 
6
6
  export type Adoption =
7
7
  | { readonly _tag: "Refused"; readonly reasons: ReadonlyArray<string> }
8
- | { readonly _tag: "Adopted"; readonly entries: ReadonlyArray<BaselineEntry>; readonly added: number };
8
+ | { readonly _tag: "Adopted"; readonly entries: ReadonlyArray<BaselineEntry>; readonly added: number; readonly replaced: number };
9
9
 
10
10
  export interface Pruned {
11
11
  readonly entries: ReadonlyArray<BaselineEntry>;
12
+ readonly moved: number;
12
13
  readonly removed: number;
13
14
  readonly lowered: number;
14
15
  }
@@ -19,27 +20,18 @@ const record = (violations: ReadonlyArray<Violation>): ReadonlyArray<BaselineEnt
19
20
  if (first === undefined) {
20
21
  return [];
21
22
  }
22
- const tally = tallyOf(group);
23
- return [
24
- tally.measure === undefined ? { count: tally.count, file: first.file, rule: first.rule } : { ...tally, file: first.file, rule: first.rule },
25
- ];
23
+ const count = countOf(group);
24
+ return count > 0 ? [{ count, file: first.file, rule: first.rule }] : [];
26
25
  });
27
26
 
28
- const refusal = (id: string, levels: ReadonlyMap<string, Level>, existing: ReadonlyArray<BaselineEntry>): ReadonlyArray<string> => {
27
+ const refusal = (id: string, levels: ReadonlyMap<string, Level>): ReadonlyArray<string> => {
29
28
  const level = levels.get(id);
30
29
  if (level === undefined) {
31
30
  return [`${id}: no built-in or local rule has this id.`];
32
31
  }
33
- if (level !== "error") {
34
- return [`${id}: the rule is ${level}. Set it to error before baselining it.`];
35
- }
36
- return existing.some((entry) => entry.rule === id) ? [`${id}: already baselined. Its entries only shrink; fix new violations instead.`] : [];
32
+ return level === "error" ? [] : [`${id}: the rule is ${level}. Set it to error before baselining it.`];
37
33
  };
38
34
 
39
- /**
40
- * Records the error-level violations of rules being adopted. Without a baseline every rule is adopted at once; afterwards a
41
- * rule is adopted by name, once, so a baseline never grows for a rule it already covers.
42
- */
43
35
  export const adopt = (
44
36
  existing: ReadonlyArray<BaselineEntry> | undefined,
45
37
  rules: ReadonlyArray<string>,
@@ -47,37 +39,62 @@ export const adopt = (
47
39
  levels: ReadonlyMap<string, Level>,
48
40
  ): Adoption => {
49
41
  if (existing !== undefined && rules.length === 0) {
50
- return { _tag: "Refused", reasons: ["a baseline exists and only shrinks. Adopt a new rule with --rule <id>, or drop fixed debt with prune."] };
42
+ return { _tag: "Refused", reasons: ["a baseline exists. Record a rule with --rule <id>, or drop fixed debt with prune."] };
51
43
  }
52
- const reasons = rules.flatMap((id) => refusal(id, levels, existing ?? []));
44
+ const reasons = rules.flatMap((id) => refusal(id, levels));
53
45
  if (reasons.length > 0) {
54
46
  return { _tag: "Refused", reasons };
55
47
  }
48
+ const kept = (existing ?? []).filter((entry) => !rules.includes(entry.rule));
56
49
  const adopted = violations.filter((violation) => violation.level === "error" && (rules.length === 0 || rules.includes(violation.rule)));
57
50
  const added = record(adopted);
58
- return { _tag: "Adopted", added: added.length, entries: [...(existing ?? []), ...added] };
51
+ return { _tag: "Adopted", added: added.length, entries: [...kept, ...added], replaced: (existing ?? []).length - kept.length };
59
52
  };
60
53
 
61
- const shrunk = (entry: BaselineEntry, current: BaselineEntry): BaselineEntry => {
62
- const count = Math.min(entry.count, current.count);
63
- return entry.measure === undefined || current.measure === undefined
64
- ? { count, file: entry.file, rule: entry.rule }
65
- : { count, file: entry.file, measure: Math.min(entry.measure, current.measure), rule: entry.rule };
66
- };
54
+ export interface Scope {
55
+ readonly moves: ReadonlyMap<string, string>;
56
+ readonly files?: ReadonlySet<string> | undefined;
57
+ }
67
58
 
68
- const unchanged = (left: BaselineEntry, right: BaselineEntry): boolean => left.count === right.count && left.measure === right.measure;
59
+ const inScope = (entry: BaselineEntry, scope: Scope): boolean =>
60
+ scope.files === undefined || scope.files.has(entry.file) || scope.moves.has(entry.file);
61
+
62
+ const destination = (
63
+ before: BaselineEntry,
64
+ current: ReadonlyMap<string, BaselineEntry>,
65
+ moves: ReadonlyMap<string, string>,
66
+ taken: ReadonlySet<string>,
67
+ ) => {
68
+ const to = moves.get(before.file);
69
+ const key = to === undefined ? undefined : keyOf(before.rule, to);
70
+ return key === undefined || taken.has(key) ? undefined : current.get(key);
71
+ };
69
72
 
70
- /** Drops fixed debt and lowers entries to what is left. It never adds an entry or raises one. */
71
- export const prune = (existing: ReadonlyArray<BaselineEntry>, violations: ReadonlyArray<Violation>, levels: ReadonlyMap<string, Level>): Pruned => {
73
+ export const prune = (
74
+ existing: ReadonlyArray<BaselineEntry>,
75
+ violations: ReadonlyArray<Violation>,
76
+ levels: ReadonlyMap<string, Level>,
77
+ scope: Scope = { moves: new Map() },
78
+ ): Pruned => {
72
79
  const current = new Map(record(violations).map((entry) => [keyOf(entry.rule, entry.file), entry]));
80
+ const taken = new Set(existing.map((entry) => keyOf(entry.rule, entry.file)));
81
+ let moved = 0;
73
82
  const kept = existing.flatMap((before) => {
74
- const now = current.get(keyOf(before.rule, before.file));
83
+ if (!inScope(before, scope)) {
84
+ return [{ after: before, before }];
85
+ }
75
86
  const level = levels.get(before.rule);
76
- return now === undefined || level === undefined || level === "off" ? [] : [{ after: shrunk(before, now), before }];
87
+ const now = current.get(keyOf(before.rule, before.file)) ?? destination(before, current, scope.moves, taken);
88
+ if (now === undefined || level === undefined || level === "off") {
89
+ return [];
90
+ }
91
+ moved += Number(now.file !== before.file);
92
+ return [{ after: { count: Math.min(before.count, now.count), file: now.file, rule: before.rule }, before }];
77
93
  });
78
94
  return {
79
95
  entries: kept.map(({ after }) => after),
80
- lowered: kept.filter(({ after, before }) => !unchanged(before, after)).length,
96
+ lowered: kept.filter(({ after, before }) => after.count !== before.count).length,
97
+ moved,
81
98
  removed: existing.length - kept.length,
82
99
  };
83
100
  };
package/src/cli/args.ts CHANGED
@@ -5,7 +5,10 @@ import type { WarningDetail } from "../report/render.ts";
5
5
  export type Command =
6
6
  | { readonly _tag: "Lint"; readonly config: string | undefined; readonly warnings: WarningDetail }
7
7
  | { readonly _tag: "BaselineWrite"; readonly config: string | undefined; readonly rules: ReadonlyArray<string> }
8
- | { readonly _tag: "BaselinePrune"; readonly config: string | undefined }
8
+ | { readonly _tag: "BaselinePrune"; readonly config: string | undefined; readonly against: string | undefined }
9
+ | { readonly _tag: "BaselineCheck"; readonly config: string | undefined; readonly against: string | undefined }
10
+ | { readonly _tag: "BaselineTighten"; readonly config: string | undefined; readonly staged: boolean }
11
+ | { readonly _tag: "BaselineMigrate"; readonly config: string | undefined; readonly from: string | undefined }
9
12
  | { readonly _tag: "Help" };
10
13
 
11
14
  export type Parsed = { readonly _tag: "Parsed"; readonly command: Command } | { readonly _tag: "Usage"; readonly problem: string };
@@ -13,19 +16,30 @@ export type Parsed = { readonly _tag: "Parsed"; readonly command: Command } | {
13
16
  export const USAGE = `Usage:
14
17
  quality lint [--config <file>] [--warnings summary|all]
15
18
  quality baseline write [--config <file>] [--rule <id>]...
16
- quality baseline prune [--config <file>]
19
+ quality baseline prune [--config <file>] [--against <ref>]
20
+ quality baseline tighten [--config <file>] [--staged]
21
+ quality baseline check [--config <file>] [--against <ref>]
22
+ quality baseline migrate [--config <file>] [--from <file>]
17
23
 
18
- lint Run every rule. Exits 1 on an error-level violation, a baseline regression or a stale entry.
19
- baseline write Record current error-level violations. Creates the baseline, or adopts named rules into an existing one.
20
- baseline prune Drop fixed debt and lower entries to what is left. Never adds or raises an entry.
24
+ lint Run every rule. Exits 1 on an error-level violation, a file over its baseline, a stale registry entry
25
+ or a baseline entry for a rule that is off or unknown.
26
+ baseline write Record current error-level violations. Creates the baseline, or records the named rules again, replacing their entries.
27
+ 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.
28
+ baseline tighten Prune only the entries of files changed since HEAD, or with --staged, in the index.
29
+ baseline check Compare the baseline with its version at the merge base. Exits 1 when it gained an entry or a higher count.
30
+ baseline migrate Move a baseline from the earlier JSON format (--from, quality/baseline.json by default) to the configured file.
21
31
 
22
32
  --config <file> Config file; its directory is the repository root. Defaults to ./quality.config.ts.
33
+ --against <ref> The branch the work merges into. Defaults to origin/HEAD, then origin/main, then origin/master.
23
34
  Exit codes: 0 passed, 1 failed the gate, 2 could not run.`;
24
35
 
25
36
  const OPTIONS = {
37
+ against: { type: "string" },
26
38
  config: { type: "string" },
39
+ from: { type: "string" },
27
40
  help: { short: "h", type: "boolean" },
28
41
  rule: { multiple: true, type: "string" },
42
+ staged: { type: "boolean" },
29
43
  warnings: { type: "string" },
30
44
  } as const;
31
45
 
@@ -35,27 +49,48 @@ const parsed = (command: Command): Parsed => ({ _tag: "Parsed", command });
35
49
 
36
50
  type Values = ReturnType<typeof parseArgs<{ options: typeof OPTIONS; allowPositionals: true }>>["values"];
37
51
 
38
- const lint = (values: Values): Parsed => {
39
- if (values.rule !== undefined) {
40
- return usage("--rule applies only to baseline write.");
41
- }
42
- const warnings = values.warnings ?? "summary";
43
- return warnings === "summary" || warnings === "all"
44
- ? parsed({ _tag: "Lint", config: values.config, warnings })
45
- : usage(`--warnings takes summary or all, not "${warnings}".`);
52
+ type Option = Exclude<keyof Values, "config" | "help">;
53
+
54
+ const COMMAND_OPTIONS: ReadonlyArray<Option> = ["against", "from", "rule", "staged", "warnings"];
55
+
56
+ const ACCEPTS: Readonly<Record<string, ReadonlyArray<Option>>> = {
57
+ lint: ["warnings"],
58
+ "baseline write": ["rule"],
59
+ "baseline prune": ["against"],
60
+ "baseline tighten": ["staged"],
61
+ "baseline check": ["against"],
62
+ "baseline migrate": ["from"],
46
63
  };
47
64
 
48
- const baseline = (action: string | undefined, values: Values): Parsed => {
49
- if (values.warnings !== undefined) {
50
- return usage("--warnings applies only to lint.");
51
- }
52
- if (action === "write") {
53
- return parsed({ _tag: "BaselineWrite", config: values.config, rules: values.rule ?? [] });
54
- }
55
- if (action === "prune" && values.rule === undefined) {
56
- return parsed({ _tag: "BaselinePrune", config: values.config });
65
+ const misplaced = (name: string, values: Values): string | undefined => {
66
+ const option = COMMAND_OPTIONS.find((key) => values[key] !== undefined && !(ACCEPTS[name] ?? []).includes(key));
67
+ const owners = Object.keys(ACCEPTS).filter((owner) => option !== undefined && (ACCEPTS[owner] ?? []).includes(option));
68
+ return option === undefined ? undefined : `--${option} applies only to ${owners.join(" and ")}.`;
69
+ };
70
+
71
+ const ref = (against: string | undefined, command: Command): Parsed =>
72
+ against?.startsWith("-") === true ? usage(`--against takes a git ref, not "${against}".`) : parsed(command);
73
+
74
+ const commandFor = (name: string, values: Values): Parsed => {
75
+ const config = values.config;
76
+ switch (name) {
77
+ case "lint": {
78
+ const warnings = values.warnings ?? "summary";
79
+ return warnings === "summary" || warnings === "all"
80
+ ? parsed({ _tag: "Lint", config, warnings })
81
+ : usage(`--warnings takes summary or all, not "${warnings}".`);
82
+ }
83
+ case "baseline write":
84
+ return parsed({ _tag: "BaselineWrite", config, rules: values.rule ?? [] });
85
+ case "baseline prune":
86
+ return ref(values.against, { _tag: "BaselinePrune", against: values.against, config });
87
+ case "baseline check":
88
+ return ref(values.against, { _tag: "BaselineCheck", against: values.against, config });
89
+ case "baseline tighten":
90
+ return parsed({ _tag: "BaselineTighten", config, staged: values.staged === true });
91
+ default:
92
+ return parsed({ _tag: "BaselineMigrate", config, from: values.from });
57
93
  }
58
- return usage(action === "prune" ? "--rule applies only to baseline write." : "baseline takes write or prune.");
59
94
  };
60
95
 
61
96
  export const parseCommand = (args: ReadonlyArray<string>): Parsed => {
@@ -67,15 +102,14 @@ export const parseCommand = (args: ReadonlyArray<string>): Parsed => {
67
102
  return usage(result.failure);
68
103
  }
69
104
  const { positionals, values } = result.success;
70
- const [command = "lint", action, ...extra] = positionals;
105
+ const [command = "lint", ...rest] = positionals;
71
106
  if (values.help === true || command === "help") {
72
107
  return parsed({ _tag: "Help" });
73
108
  }
74
- if (command === "lint" && action === undefined) {
75
- return lint(values);
76
- }
77
- if (command === "baseline" && extra.length === 0) {
78
- return baseline(action, values);
109
+ const name = [command, ...rest].join(" ");
110
+ if (ACCEPTS[name] === undefined) {
111
+ return usage(command === "baseline" && rest.length < 2 ? "baseline takes write, prune, tighten, check or migrate." : `unknown command: ${name}`);
79
112
  }
80
- return usage(`unknown command: ${positionals.join(" ")}`);
113
+ const problem = misplaced(name, values);
114
+ return problem === undefined ? commandFor(name, values) : usage(problem);
81
115
  };
@@ -1,7 +1,8 @@
1
1
  import { join } from "node:path";
2
2
  import { Console, Effect, type FileSystem } from "effect";
3
- import { type BaselineEntry, encodeBaseline, indentOf } from "../baseline/format.ts";
4
- import { adopt, prune } from "../baseline/update.ts";
3
+ import type { BaselineEntry } from "../baseline/format.ts";
4
+ import { rewriteBaseline } from "../baseline/rewrite.ts";
5
+ import { adopt, type Pruned, prune, type Scope } from "../baseline/update.ts";
5
6
  import { openSession, type Session } from "../engine/session.ts";
6
7
  import { applyRegistry } from "../exceptions/registry.ts";
7
8
  import { SetupFailure } from "../failure.ts";
@@ -10,11 +11,12 @@ import { plural } from "../report/plural.ts";
10
11
 
11
12
  const save = (session: Session, entries: ReadonlyArray<BaselineEntry>): Effect.Effect<void, SetupFailure, FileSystem.FileSystem> =>
12
13
  Effect.mapError(
13
- writeText(join(session.config.root, session.config.baseline), encodeBaseline(entries, indentOf(session.baseline.raw))),
14
+ writeText(join(session.config.root, session.config.baseline), rewriteBaseline(session.baseline.raw, session.baseline.entries, entries)),
14
15
  (failure) => new SetupFailure({ message: failure.message }),
15
16
  );
16
17
 
17
- const unregistered = (session: Session) => applyRegistry(session.violations, session.registry, session.config.levels).kept;
18
+ const unregistered = (session: Session) =>
19
+ applyRegistry(session.violations, session.registry, session.config.levels, session.config.unregistrable).kept;
18
20
 
19
21
  export const writeBaseline = (
20
22
  cwd: string,
@@ -28,25 +30,37 @@ export const writeBaseline = (
28
30
  if (adoption._tag === "Refused") {
29
31
  return yield* new SetupFailure({ message: `baseline write refused:\n${adoption.reasons.map((reason) => ` - ${reason}`).join("\n")}` });
30
32
  }
31
- if (adoption.added === 0) {
33
+ if (adoption.added === 0 && adoption.replaced === 0) {
32
34
  return yield* Console.log(`quality: no error-level violations to record; ${session.config.baseline} is unchanged.`);
33
35
  }
34
36
  yield* save(session, adoption.entries);
35
- yield* Console.log(`quality: recorded ${plural(adoption.added, "entry", "entries")} in ${session.config.baseline}.`);
37
+ const replaced = adoption.replaced > 0 ? `, replacing ${plural(adoption.replaced, "entry", "entries")}` : "";
38
+ yield* Console.log(`quality: recorded ${plural(adoption.added, "entry", "entries")} in ${session.config.baseline}${replaced}.`);
36
39
  });
37
40
 
38
- export const pruneBaseline = (cwd: string, config: string | undefined): Effect.Effect<void, SetupFailure, FileSystem.FileSystem> =>
41
+ const changes = (pruned: Pruned): string => {
42
+ const parts = [
43
+ `removed ${plural(pruned.removed, "entry", "entries")}`,
44
+ `lowered ${plural(pruned.lowered, "entry", "entries")}`,
45
+ ...(pruned.moved > 0 ? [`carried ${plural(pruned.moved, "entry", "entries")} to moved files`] : []),
46
+ ];
47
+ return `${parts.slice(0, -1).join(", ")} and ${parts.at(-1)}`;
48
+ };
49
+
50
+ export const shrinkBaseline = <Requirements>(
51
+ cwd: string,
52
+ config: string | undefined,
53
+ scopeOf: (session: Session) => Effect.Effect<Scope, SetupFailure, Requirements>,
54
+ ): Effect.Effect<void, SetupFailure, FileSystem.FileSystem | Requirements> =>
39
55
  Effect.gen(function* () {
40
56
  const session = yield* openSession(cwd, config);
41
57
  if (session.baseline.raw === undefined) {
42
- return yield* Console.log(`quality: no baseline at ${session.config.baseline}; nothing to prune.`);
58
+ return yield* Console.log(`quality: no baseline at ${session.config.baseline}; nothing to lower.`);
43
59
  }
44
- const pruned = prune(session.baseline.entries, unregistered(session), session.config.levels);
45
- if (pruned.removed === 0 && pruned.lowered === 0) {
60
+ const pruned = prune(session.baseline.entries, unregistered(session), session.config.levels, yield* scopeOf(session));
61
+ if (pruned.removed === 0 && pruned.lowered === 0 && pruned.moved === 0) {
46
62
  return yield* Console.log(`quality: ${session.config.baseline} is current.`);
47
63
  }
48
64
  yield* save(session, pruned.entries);
49
- yield* Console.log(
50
- `quality: removed ${plural(pruned.removed, "entry", "entries")} and lowered ${plural(pruned.lowered, "entry", "entries")} in ${session.config.baseline}.`,
51
- );
65
+ yield* Console.log(`quality: ${changes(pruned)} in ${session.config.baseline}.`);
52
66
  });
@@ -0,0 +1,51 @@
1
+ import { relative } from "node:path";
2
+ import { Console, Effect, type FileSystem } from "effect";
3
+ import { type BaselineEntry, decodeBaseline } from "../baseline/format.ts";
4
+ import { guardBaseline } from "../baseline/guard.ts";
5
+ import { convertLegacy, decodeLegacyBaseline, LEGACY_BASELINE, measured } from "../baseline/legacy.ts";
6
+ import { loadConfig, type ResolvedConfig } from "../config/load.ts";
7
+ import { validOrFail } from "../decoded.ts";
8
+ import { readBaseline } from "../engine/baseline-file.ts";
9
+ import { scan } from "../engine/session.ts";
10
+ import { GateFailed, type SetupFailure } from "../failure.ts";
11
+ import { type Base, resolveBase } from "../git/base.ts";
12
+ import type { Git } from "../git/command.ts";
13
+ import { changesSince, readAt } from "../git/history.ts";
14
+ import { renderGuard } from "../report/guard.ts";
15
+
16
+ const baseEntries = (
17
+ config: ResolvedConfig,
18
+ base: Base,
19
+ path: string,
20
+ ): Effect.Effect<ReadonlyArray<BaselineEntry>, SetupFailure, FileSystem.FileSystem | Git> =>
21
+ Effect.gen(function* () {
22
+ const raw = yield* readAt(config.root, base.commit, path);
23
+ if (raw !== undefined) {
24
+ return yield* validOrFail(`${path} at ${base.ref}`, yield* Effect.promise(() => decodeBaseline(raw)));
25
+ }
26
+ const legacyRaw = yield* readAt(config.root, base.commit, LEGACY_BASELINE);
27
+ if (legacyRaw === undefined) {
28
+ return [];
29
+ }
30
+ const legacy = yield* validOrFail(`${LEGACY_BASELINE} at ${base.ref}`, yield* Effect.promise(() => decodeLegacyBaseline(legacyRaw)));
31
+ return measured(legacy) ? convertLegacy(legacy, (yield* scan(config)).violations) : legacy;
32
+ });
33
+
34
+ export const checkBaseline = (
35
+ cwd: string,
36
+ configPath: string | undefined,
37
+ against: string | undefined,
38
+ ): Effect.Effect<void, SetupFailure | GateFailed, FileSystem.FileSystem | Git> =>
39
+ Effect.gen(function* () {
40
+ const config = yield* loadConfig(cwd, configPath);
41
+ const working = yield* readBaseline(config);
42
+ const base = yield* resolveBase(config.root, against);
43
+ const { moves } = yield* changesSince(config.root, [base.commit]);
44
+ const renames = new Map([...moves].map(([from, to]) => [to, from]));
45
+ const before = yield* baseEntries(config, base, renames.get(config.baseline) ?? config.baseline);
46
+ const result = guardBaseline(before, working.entries, renames, config.adopt);
47
+ yield* Console.log(renderGuard(result, { base, baseline: config.baseline, config: relative(config.root, config.file) }));
48
+ if (result.problems.length > 0) {
49
+ return yield* new GateFailed();
50
+ }
51
+ });
package/src/cli/lint.ts CHANGED
@@ -11,7 +11,7 @@ export const lint = (
11
11
  ): Effect.Effect<void, SetupFailure | GateFailed, FileSystem.FileSystem> =>
12
12
  Effect.gen(function* () {
13
13
  const session = yield* openSession(cwd, config);
14
- const outcome = evaluate(session.violations, session.registry, session.baseline.entries, session.config.levels);
14
+ const outcome = evaluate(session.violations, session.registry, session.baseline.entries, session.config.levels, session.config.unregistrable);
15
15
  yield* Console.log(
16
16
  render(outcome, {
17
17
  baseline: session.config.baseline,
@@ -0,0 +1,36 @@
1
+ import { join } from "node:path";
2
+ import { Console, Effect, FileSystem } from "effect";
3
+ import { encodeBaseline } from "../baseline/format.ts";
4
+ import { convertLegacy, decodeLegacyBaseline, LEGACY_BASELINE } from "../baseline/legacy.ts";
5
+ import { loadConfig } from "../config/load.ts";
6
+ import { validOrFail } from "../decoded.ts";
7
+ import { readInput } from "../engine/baseline-file.ts";
8
+ import { scan } from "../engine/session.ts";
9
+ import { SetupFailure } from "../failure.ts";
10
+ import { writeText } from "../inventory/filesystem.ts";
11
+ import { plural } from "../report/plural.ts";
12
+
13
+ export const migrateBaseline = (
14
+ cwd: string,
15
+ configPath: string | undefined,
16
+ from: string | undefined,
17
+ ): Effect.Effect<void, SetupFailure, FileSystem.FileSystem> =>
18
+ Effect.gen(function* () {
19
+ const config = yield* loadConfig(cwd, configPath);
20
+ const source = from ?? LEGACY_BASELINE;
21
+ if (source === config.baseline || (yield* readInput(config.root, config.baseline)) !== undefined) {
22
+ return yield* new SetupFailure({ message: `${config.baseline} already exists. Migrate into a baseline file that does not exist yet.` });
23
+ }
24
+ const raw = yield* readInput(config.root, source);
25
+ if (raw === undefined) {
26
+ return yield* new SetupFailure({ message: `no baseline at ${source} to migrate. Name it with --from <file>.` });
27
+ }
28
+ const legacy = yield* validOrFail(source, yield* Effect.promise(() => decodeLegacyBaseline(raw)));
29
+ const entries = convertLegacy(legacy, (yield* scan(config)).violations);
30
+ const fs = yield* FileSystem.FileSystem;
31
+ const written = Effect.andThen(writeText(join(config.root, config.baseline), encodeBaseline(entries)), fs.remove(join(config.root, source)));
32
+ yield* Effect.mapError(written, (failure) => new SetupFailure({ message: failure.message }));
33
+ const fixed = legacy.length - entries.length;
34
+ const dropped = fixed > 0 ? `, dropping ${plural(fixed, "fixed entry", "fixed entries")}` : "";
35
+ yield* Console.log(`quality: moved ${plural(entries.length, "entry", "entries")} from ${source} to ${config.baseline}${dropped}.`);
36
+ });