@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.
- package/CHANGELOG.md +105 -0
- package/README.md +207 -35
- package/dist/baseline/compare.d.ts +3 -8
- package/dist/baseline/compare.d.ts.map +1 -1
- package/dist/baseline/compare.js +13 -23
- package/dist/baseline/compare.js.map +1 -1
- package/dist/baseline/format.d.ts +7 -5
- package/dist/baseline/format.d.ts.map +1 -1
- package/dist/baseline/format.js +31 -23
- package/dist/baseline/format.js.map +1 -1
- package/dist/baseline/guard.d.ts +23 -0
- package/dist/baseline/guard.d.ts.map +1 -0
- package/dist/baseline/guard.js +31 -0
- package/dist/baseline/guard.js.map +1 -0
- package/dist/baseline/legacy.d.ts +11 -0
- package/dist/baseline/legacy.d.ts.map +1 -0
- package/dist/baseline/legacy.js +33 -0
- package/dist/baseline/legacy.js.map +1 -0
- package/dist/baseline/rewrite.d.ts +3 -0
- package/dist/baseline/rewrite.d.ts.map +1 -0
- package/dist/baseline/rewrite.js +19 -0
- package/dist/baseline/rewrite.js.map +1 -0
- package/dist/baseline/update.d.ts +7 -6
- package/dist/baseline/update.d.ts.map +1 -1
- package/dist/baseline/update.js +28 -28
- package/dist/baseline/update.js.map +1 -1
- package/dist/cli/args.d.ts +14 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +54 -29
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/baseline.d.ts +3 -1
- package/dist/cli/baseline.d.ts.map +1 -1
- package/dist/cli/baseline.js +19 -10
- package/dist/cli/baseline.js.map +1 -1
- package/dist/cli/check.d.ts +5 -0
- package/dist/cli/check.d.ts.map +1 -0
- package/dist/cli/check.js +39 -0
- package/dist/cli/check.js.map +1 -0
- package/dist/cli/lint.js +1 -1
- package/dist/cli/lint.js.map +1 -1
- package/dist/cli/migrate.d.ts +4 -0
- package/dist/cli/migrate.d.ts.map +1 -0
- package/dist/cli/migrate.js +31 -0
- package/dist/cli/migrate.js.map +1 -0
- package/dist/cli/run-main.d.ts +2 -1
- package/dist/cli/run-main.d.ts.map +1 -1
- package/dist/cli/run-main.js +5 -2
- package/dist/cli/run-main.js.map +1 -1
- package/dist/cli/shrink.d.ts +6 -0
- package/dist/cli/shrink.d.ts.map +1 -0
- package/dist/cli/shrink.js +8 -0
- package/dist/cli/shrink.js.map +1 -0
- package/dist/cli.js +11 -2
- package/dist/cli.js.map +1 -1
- package/dist/config/decode.d.ts +1 -0
- package/dist/config/decode.d.ts.map +1 -1
- package/dist/config/decode.js +1 -0
- package/dist/config/decode.js.map +1 -1
- package/dist/config/file.d.ts +2 -0
- package/dist/config/file.d.ts.map +1 -0
- package/dist/config/file.js +2 -0
- package/dist/config/file.js.map +1 -0
- package/dist/config/load.d.ts +1 -1
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +3 -2
- package/dist/config/load.js.map +1 -1
- package/dist/config/resolve.d.ts +1 -0
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +6 -4
- package/dist/config/resolve.js.map +1 -1
- package/dist/config.d.ts +2 -7
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/engine/baseline-file.d.ts +11 -0
- package/dist/engine/baseline-file.d.ts.map +1 -0
- package/dist/engine/baseline-file.js +18 -0
- package/dist/engine/baseline-file.js.map +1 -0
- package/dist/engine/evaluate.d.ts +2 -2
- package/dist/engine/evaluate.d.ts.map +1 -1
- package/dist/engine/evaluate.js +3 -3
- package/dist/engine/evaluate.js.map +1 -1
- package/dist/engine/session.d.ts +3 -5
- package/dist/engine/session.d.ts.map +1 -1
- package/dist/engine/session.js +12 -10
- package/dist/engine/session.js.map +1 -1
- package/dist/exceptions/registry.d.ts +1 -1
- package/dist/exceptions/registry.d.ts.map +1 -1
- package/dist/exceptions/registry.js +8 -3
- package/dist/exceptions/registry.js.map +1 -1
- package/dist/failure.d.ts +0 -2
- package/dist/failure.d.ts.map +1 -1
- package/dist/failure.js +0 -2
- package/dist/failure.js.map +1 -1
- package/dist/git/base.d.ts +9 -0
- package/dist/git/base.d.ts.map +1 -0
- package/dist/git/base.js +34 -0
- package/dist/git/base.js.map +1 -0
- package/dist/git/command.d.ts +12 -0
- package/dist/git/command.d.ts.map +1 -0
- package/dist/git/command.js +13 -0
- package/dist/git/command.js.map +1 -0
- package/dist/git/history.d.ts +10 -0
- package/dist/git/history.d.ts.map +1 -0
- package/dist/git/history.js +28 -0
- package/dist/git/history.js.map +1 -0
- package/dist/inventory/ignore-scope.d.ts.map +1 -1
- package/dist/inventory/ignore-scope.js +0 -1
- package/dist/inventory/ignore-scope.js.map +1 -1
- package/dist/inventory/walk.d.ts +0 -1
- package/dist/inventory/walk.d.ts.map +1 -1
- package/dist/inventory/walk.js +1 -1
- package/dist/inventory/walk.js.map +1 -1
- package/dist/report/guard.d.ts +9 -0
- package/dist/report/guard.d.ts.map +1 -0
- package/dist/report/guard.js +34 -0
- package/dist/report/guard.js.map +1 -0
- package/dist/report/render.js +1 -2
- package/dist/report/render.js.map +1 -1
- package/dist/report/stale.d.ts.map +1 -1
- package/dist/report/stale.js +4 -3
- package/dist/report/stale.js.map +1 -1
- package/dist/rule.d.ts +4 -7
- package/dist/rule.d.ts.map +1 -1
- package/dist/rule.js +1 -1
- package/dist/rule.js.map +1 -1
- package/dist/rules/built-in.d.ts +17 -0
- package/dist/rules/built-in.d.ts.map +1 -1
- package/dist/rules/built-in.js +21 -1
- package/dist/rules/built-in.js.map +1 -1
- package/dist/rules/comments/kinds.d.ts +9 -0
- package/dist/rules/comments/kinds.d.ts.map +1 -0
- package/dist/rules/comments/kinds.js +34 -0
- package/dist/rules/comments/kinds.js.map +1 -0
- package/dist/rules/comments/max-per-file.d.ts +5 -0
- package/dist/rules/comments/max-per-file.d.ts.map +1 -0
- package/dist/rules/comments/max-per-file.js +39 -0
- package/dist/rules/comments/max-per-file.js.map +1 -0
- package/dist/rules/comments/no-banner.d.ts +2 -0
- package/dist/rules/comments/no-banner.d.ts.map +1 -0
- package/dist/rules/comments/no-banner.js +9 -0
- package/dist/rules/comments/no-banner.js.map +1 -0
- package/dist/rules/comments/no-jsdoc.d.ts +4 -0
- package/dist/rules/comments/no-jsdoc.d.ts.map +1 -0
- package/dist/rules/comments/no-jsdoc.js +14 -0
- package/dist/rules/comments/no-jsdoc.js.map +1 -0
- package/dist/rules/comments/no-line-reference.d.ts +2 -0
- package/dist/rules/comments/no-line-reference.d.ts.map +1 -0
- package/dist/rules/comments/no-line-reference.js +9 -0
- package/dist/rules/comments/no-line-reference.js.map +1 -0
- package/dist/rules/comments/no-pr-reference.d.ts +2 -0
- package/dist/rules/comments/no-pr-reference.d.ts.map +1 -0
- package/dist/rules/comments/no-pr-reference.js +13 -0
- package/dist/rules/comments/no-pr-reference.js.map +1 -0
- package/dist/rules/comments/no-todo.d.ts +2 -0
- package/dist/rules/comments/no-todo.d.ts.map +1 -0
- package/dist/rules/comments/no-todo.js +8 -0
- package/dist/rules/comments/no-todo.js.map +1 -0
- package/dist/rules/comments/pattern-rule.d.ts +10 -0
- package/dist/rules/comments/pattern-rule.d.ts.map +1 -0
- package/dist/rules/comments/pattern-rule.js +16 -0
- package/dist/rules/comments/pattern-rule.js.map +1 -0
- package/dist/rules/comments/scan.d.ts +13 -0
- package/dist/rules/comments/scan.d.ts.map +1 -0
- package/dist/rules/comments/scan.js +62 -0
- package/dist/rules/comments/scan.js.map +1 -0
- package/dist/rules/max-lines.d.ts.map +1 -1
- package/dist/rules/max-lines.js +8 -1
- package/dist/rules/max-lines.js.map +1 -1
- package/dist/rules/suppressions/biome/configs.d.ts +14 -0
- package/dist/rules/suppressions/biome/configs.d.ts.map +1 -0
- package/dist/rules/suppressions/biome/configs.js +39 -0
- package/dist/rules/suppressions/biome/configs.js.map +1 -0
- package/dist/rules/suppressions/biome/json.d.ts +27 -0
- package/dist/rules/suppressions/biome/json.d.ts.map +1 -0
- package/dist/rules/suppressions/biome/json.js +37 -0
- package/dist/rules/suppressions/biome/json.js.map +1 -0
- package/dist/rules/suppressions/biome/weakenings.d.ts +9 -0
- package/dist/rules/suppressions/biome/weakenings.d.ts.map +1 -0
- package/dist/rules/suppressions/biome/weakenings.js +68 -0
- package/dist/rules/suppressions/biome/weakenings.js.map +1 -0
- package/dist/rules/suppressions/biome-overrides.d.ts +8 -0
- package/dist/rules/suppressions/biome-overrides.d.ts.map +1 -0
- package/dist/rules/suppressions/biome-overrides.js +47 -0
- package/dist/rules/suppressions/biome-overrides.js.map +1 -0
- package/dist/rules/suppressions/directives.d.ts +4 -0
- package/dist/rules/suppressions/directives.d.ts.map +1 -0
- package/dist/rules/suppressions/directives.js +19 -0
- package/dist/rules/suppressions/directives.js.map +1 -0
- package/dist/rules/suppressions/no-double-cast.d.ts +2 -0
- package/dist/rules/suppressions/no-double-cast.d.ts.map +1 -0
- package/dist/rules/suppressions/no-double-cast.js +38 -0
- package/dist/rules/suppressions/no-double-cast.js.map +1 -0
- package/dist/rules/suppressions/no-inline.d.ts +8 -0
- package/dist/rules/suppressions/no-inline.d.ts.map +1 -0
- package/dist/rules/suppressions/no-inline.js +48 -0
- package/dist/rules/suppressions/no-inline.js.map +1 -0
- package/dist/rules/suppressions/stylesheet-comments.d.ts +4 -0
- package/dist/rules/suppressions/stylesheet-comments.d.ts.map +1 -0
- package/dist/rules/suppressions/stylesheet-comments.js +20 -0
- package/dist/rules/suppressions/stylesheet-comments.js.map +1 -0
- package/dist/rules/syntax.d.ts +4 -0
- package/dist/rules/syntax.d.ts.map +1 -0
- package/dist/rules/syntax.js +12 -0
- package/dist/rules/syntax.js.map +1 -0
- package/package.json +4 -5
- package/src/baseline/compare.ts +15 -39
- package/src/baseline/format.ts +35 -32
- package/src/baseline/guard.ts +47 -0
- package/src/baseline/legacy.ts +46 -0
- package/src/baseline/rewrite.ts +24 -0
- package/src/baseline/update.ts +47 -30
- package/src/cli/args.ts +64 -30
- package/src/cli/baseline.ts +27 -13
- package/src/cli/check.ts +51 -0
- package/src/cli/lint.ts +1 -1
- package/src/cli/migrate.ts +36 -0
- package/src/cli/run-main.ts +8 -3
- package/src/cli/shrink.ts +31 -0
- package/src/cli.ts +11 -2
- package/src/config/decode.ts +1 -0
- package/src/config/file.ts +1 -0
- package/src/config/load.ts +4 -3
- package/src/config/resolve.ts +7 -4
- package/src/config.ts +3 -7
- package/src/engine/baseline-file.ts +27 -0
- package/src/engine/evaluate.ts +4 -2
- package/src/engine/session.ts +14 -12
- package/src/exceptions/registry.ts +9 -2
- package/src/failure.ts +0 -2
- package/src/git/base.ts +54 -0
- package/src/git/command.ts +31 -0
- package/src/git/history.ts +41 -0
- package/src/inventory/ignore-scope.ts +0 -1
- package/src/inventory/walk.ts +1 -1
- package/src/report/guard.ts +46 -0
- package/src/report/render.ts +2 -2
- package/src/report/stale.ts +11 -3
- package/src/rule.ts +5 -8
- package/src/rules/built-in.ts +21 -1
- package/src/rules/comments/kinds.ts +45 -0
- package/src/rules/comments/max-per-file.ts +46 -0
- package/src/rules/comments/no-banner.ts +10 -0
- package/src/rules/comments/no-jsdoc.ts +18 -0
- package/src/rules/comments/no-line-reference.ts +11 -0
- package/src/rules/comments/no-pr-reference.ts +13 -0
- package/src/rules/comments/no-todo.ts +8 -0
- package/src/rules/comments/pattern-rule.ts +28 -0
- package/src/rules/comments/scan.ts +80 -0
- package/src/rules/max-lines.ts +8 -1
- package/src/rules/suppressions/biome/configs.ts +50 -0
- package/src/rules/suppressions/biome/json.ts +55 -0
- package/src/rules/suppressions/biome/weakenings.ts +102 -0
- package/src/rules/suppressions/biome-overrides.ts +61 -0
- package/src/rules/suppressions/directives.ts +25 -0
- package/src/rules/suppressions/no-double-cast.ts +47 -0
- package/src/rules/suppressions/no-inline.ts +63 -0
- package/src/rules/suppressions/stylesheet-comments.ts +27 -0
- package/src/rules/syntax.ts +14 -0
package/src/baseline/format.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
25
|
-
codepoints(left.
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
53
|
-
|
|
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
|
+
};
|
package/src/baseline/update.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 ?? [])
|
|
51
|
+
return { _tag: "Adopted", added: added.length, entries: [...kept, ...added], replaced: (existing ?? []).length - kept.length };
|
|
59
52
|
};
|
|
60
53
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
83
|
+
if (!inScope(before, scope)) {
|
|
84
|
+
return [{ after: before, before }];
|
|
85
|
+
}
|
|
75
86
|
const level = levels.get(before.rule);
|
|
76
|
-
|
|
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 }) =>
|
|
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
|
|
19
|
-
baseline
|
|
20
|
-
baseline
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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",
|
|
105
|
+
const [command = "lint", ...rest] = positionals;
|
|
71
106
|
if (values.help === true || command === "help") {
|
|
72
107
|
return parsed({ _tag: "Help" });
|
|
73
108
|
}
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
113
|
+
const problem = misplaced(name, values);
|
|
114
|
+
return problem === undefined ? commandFor(name, values) : usage(problem);
|
|
81
115
|
};
|
package/src/cli/baseline.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { Console, Effect, type FileSystem } from "effect";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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),
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
});
|
package/src/cli/check.ts
ADDED
|
@@ -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
|
+
});
|