@avi2dg/checks 0.18.0 → 0.19.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.
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env bun
2
+ import { Console, Effect, Schema } from "effect";
3
+ import { commitOf, git, isShallowBoundary, refArgs } from "./git.ts";
4
+ import { runMain } from "./main.ts";
5
+ import { TEST_FILE } from "./test-layout.ts";
6
+
7
+ export const QUARANTINE = "tests/quarantine/";
8
+ export const QUARANTINE_DAYS = 30;
9
+ const DAY_SECONDS = 86400;
10
+
11
+ export class QuarantineError extends Schema.TaggedError<QuarantineError>()("QuarantineError", {
12
+ message: Schema.String,
13
+ }) {}
14
+
15
+ const USAGE = "usage: quarantine-clock.ts <ref> | <base-ref> <head-ref>";
16
+
17
+ export type Entry =
18
+ | { readonly kind: "entered"; readonly sha: string; readonly at: number; readonly day: string }
19
+ | { readonly kind: "truncated" };
20
+
21
+ export type Overdue = {
22
+ readonly file: string;
23
+ readonly day: string;
24
+ readonly days: number;
25
+ };
26
+
27
+ export type ClockResult = {
28
+ readonly checked: number;
29
+ readonly overdue: readonly Overdue[];
30
+ };
31
+
32
+ const HEADER = /^commit ([0-9a-f]{40}|[0-9a-f]{64}) (\d+) (\S+)$/;
33
+ const TWO_PATHS = /^[RC]/;
34
+
35
+ type Status = readonly [status: string, from: string, to: string];
36
+
37
+ type CommitBlock = {
38
+ readonly sha: string;
39
+ readonly at: number;
40
+ readonly day: string;
41
+ readonly statuses: readonly Status[];
42
+ };
43
+
44
+ function parseBlocks(output: string): readonly CommitBlock[] {
45
+ const blocks: { readonly sha: string; readonly at: number; readonly day: string; readonly statuses: Status[] }[] = [];
46
+ const fields = output.split("\0");
47
+ let index = 0;
48
+ while (index < fields.length) {
49
+ const field = (fields[index] ?? "").replace(/^\n/, "");
50
+ index += 1;
51
+ const header = HEADER.exec(field);
52
+ if (header !== null) {
53
+ blocks.push({ sha: header[1] ?? "", at: Number(header[2] ?? "0"), day: (header[3] ?? "").slice(0, "YYYY-MM-DD".length), statuses: [] });
54
+ continue;
55
+ }
56
+ if (field === "") continue;
57
+ const paths = TWO_PATHS.test(field) ? 2 : 1;
58
+ blocks.at(-1)?.statuses.push([field, fields[index] ?? "", fields[index + paths - 1] ?? ""]);
59
+ index += paths;
60
+ }
61
+ return blocks;
62
+ }
63
+
64
+ function entryIn({ sha, at, day, statuses }: CommitBlock, path: string): Entry | undefined {
65
+ const entered = statuses.some(
66
+ ([status, from, to]) =>
67
+ to === path && (status === "A" || status.startsWith("C") || (status.startsWith("R") && !from.startsWith(QUARANTINE))),
68
+ );
69
+ return entered ? { kind: "entered", sha, at, day } : undefined;
70
+ }
71
+
72
+ function followedPath({ statuses }: CommitBlock, path: string): string {
73
+ const rename = statuses.find(([status, , to]) => status.startsWith("R") && to === path);
74
+ return rename?.[1] ?? path;
75
+ }
76
+
77
+ export function findEntry(output: string, file: string): Entry {
78
+ let current = file;
79
+ for (const block of parseBlocks(output)) {
80
+ const entry = entryIn(block, current);
81
+ if (entry !== undefined) return entry;
82
+ current = followedPath(block, current);
83
+ }
84
+ return { kind: "truncated" };
85
+ }
86
+
87
+ export function overdueOf(
88
+ entries: readonly { readonly file: string; readonly at: number; readonly day: string }[],
89
+ headAt: number,
90
+ ): readonly Overdue[] {
91
+ const limit = QUARANTINE_DAYS * DAY_SECONDS;
92
+ return entries
93
+ .flatMap((entry) =>
94
+ headAt - entry.at > limit
95
+ ? [{ file: entry.file, day: entry.day, days: Math.floor((headAt - entry.at) / DAY_SECONDS) }]
96
+ : [],
97
+ )
98
+ .toSorted((a, b) => (a.file < b.file ? -1 : 1));
99
+ }
100
+
101
+ export function report({ checked, overdue }: ClockResult): string {
102
+ if (overdue.length === 0) {
103
+ return `quarantine-clock: no test in ${QUARANTINE} is past ${QUARANTINE_DAYS} days (${checked} checked)`;
104
+ }
105
+ const verb = overdue.length === 1 ? "is" : "are";
106
+ return [
107
+ `quarantine-clock: ${overdue.length} test(s) in ${QUARANTINE} ${verb} past ${QUARANTINE_DAYS} days; fix each and move it back, or delete it:`,
108
+ ...overdue.map((file) => ` ${file.file} entered quarantine on ${file.day} (${file.days} days ago)`),
109
+ ].join("\n");
110
+ }
111
+
112
+ const filesAt = Effect.fn("filesAt")(function* (root: string, head: string) {
113
+ const listed = yield* git(["ls-tree", "-r", "-z", "--name-only", head, "--", QUARANTINE], root);
114
+ return listed.split("\0").filter((file) => TEST_FILE.test(file));
115
+ });
116
+
117
+ const entryAt = Effect.fn("entryAt")(function* (root: string, head: string, file: string) {
118
+ const output = yield* git(["log", "--follow", "--root", "--name-status", "-z", "--format=commit %H %at %aI", head, "--", file], root);
119
+ const entry = findEntry(output, file);
120
+ if (entry.kind === "truncated" || (yield* isShallowBoundary(entry.sha, root))) {
121
+ return yield* new QuarantineError({
122
+ message: `cannot see ${file} entering ${QUARANTINE}; fetch the whole history, since a shallow clone ends before it`,
123
+ });
124
+ }
125
+ return { file, at: entry.at, day: entry.day };
126
+ });
127
+
128
+ const headAt = Effect.fn("headAt")(function* (root: string, head: string) {
129
+ const shown = yield* git(["show", "-s", "--format=%at %ct", head], root);
130
+ const dates = shown.trim().split(" ").map(Number);
131
+ if (dates.length !== 2 || !dates.every(Number.isInteger)) {
132
+ return yield* new QuarantineError({ message: `cannot read when ${head} was written` });
133
+ }
134
+ return Math.max(...dates);
135
+ });
136
+
137
+ export const runHead = Effect.fn("runHead")(function* (root: string, head: string) {
138
+ const files = yield* filesAt(root, head);
139
+ const entries = yield* Effect.forEach(files, (file) => entryAt(root, head, file));
140
+ return { checked: files.length, overdue: overdueOf(entries, yield* headAt(root, head)) } satisfies ClockResult;
141
+ });
142
+
143
+ const clock = Effect.gen(function* () {
144
+ const { first, second } = yield* refArgs(process.argv.slice(2), USAGE);
145
+ if (second !== undefined) yield* commitOf(first);
146
+ const head = yield* commitOf(second ?? first);
147
+ const root = (yield* git(["rev-parse", "--show-toplevel"])).trim();
148
+ const result = yield* runHead(root, head);
149
+ yield* Console.log(report(result));
150
+ return result.overdue.length === 0;
151
+ });
152
+
153
+ if (import.meta.main) runMain("quarantine-clock", clock);
@@ -4,22 +4,22 @@ import { changedPaths, checkoutFiles, collect, git, pathsAt, rangeFromArgs } fro
4
4
  import { runMain } from "./main.ts";
5
5
  import { readQuality } from "./quality-file.ts";
6
6
 
7
- type Fragment = {
7
+ export type Fragment = {
8
8
  readonly file: string;
9
9
  readonly start: number;
10
10
  readonly end: number;
11
11
  };
12
12
 
13
- type Clone = readonly [Fragment, Fragment];
13
+ export type Clone = readonly [Fragment, Fragment];
14
14
 
15
- type Rise = {
15
+ export type Rise = {
16
16
  readonly file: string;
17
17
  readonly before: number;
18
18
  readonly after: number;
19
19
  readonly clones: readonly Clone[];
20
20
  };
21
21
 
22
- type Held = {
22
+ export type Held = {
23
23
  readonly measured: number;
24
24
  readonly rises: readonly Rise[];
25
25
  readonly advisory: ReadonlyMap<string, number>;
@@ -86,7 +86,7 @@ const scan = Effect.fn("scan")(
86
86
  Effect.scoped,
87
87
  );
88
88
 
89
- function clonesOf(file: string, clones: readonly Clone[]): readonly Clone[] {
89
+ export function clonesOf(file: string, clones: readonly Clone[]): readonly Clone[] {
90
90
  return clones
91
91
  .flatMap(([first, second]): Clone[] => {
92
92
  if (first.file === file) return [[first, second]];
@@ -95,6 +95,36 @@ function clonesOf(file: string, clones: readonly Clone[]): readonly Clone[] {
95
95
  .toSorted(([a], [b]) => a.start - b.start);
96
96
  }
97
97
 
98
+ // Decides which held file repeats more lines than the same path, or the path it was renamed
99
+ // from, repeated at the base.
100
+ export function risesOf(
101
+ held: readonly string[],
102
+ before: ReadonlyMap<string, number>,
103
+ after: ReadonlyMap<string, number>,
104
+ formerPath: ReadonlyMap<string, string>,
105
+ clones: readonly Clone[],
106
+ ): readonly Rise[] {
107
+ return held
108
+ .flatMap((file): Rise[] => {
109
+ const was = before.get(formerPath.get(file) ?? file) ?? 0;
110
+ const is = after.get(file) ?? 0;
111
+ return is > was ? [{ file, before: was, after: is, clones: clonesOf(file, clones) }] : [];
112
+ })
113
+ .toSorted((a, b) => a.file.localeCompare(b.file));
114
+ }
115
+
116
+ // Tallies every held file that did not rise, plus every file outside the held set, as advisory.
117
+ export function heldOf(
118
+ measured: number,
119
+ rises: readonly Rise[],
120
+ after: ReadonlyMap<string, number>,
121
+ othersRepeated: ReadonlyMap<string, number>,
122
+ ): Held {
123
+ const risen = new Set(rises.map((rise) => rise.file));
124
+ const advisory = [...after].filter(([file]) => !risen.has(file)).concat([...othersRepeated]);
125
+ return { measured, rises, advisory: new Map(advisory.toSorted(([a], [b]) => a.localeCompare(b))) };
126
+ }
127
+
98
128
  const runHold = Effect.fn("runHold")(function* (root: string, production: readonly string[], base: string, head: string) {
99
129
  const pathspecs = [...production.map((glob) => `:(glob)${glob}`), DECLARATIONS];
100
130
  const held = yield* pathsAt(head, pathspecs, root);
@@ -107,28 +137,19 @@ const runHold = Effect.fn("runHold")(function* (root: string, production: readon
107
137
  const before = repeatedLines(yield* scan(root, base, yield* pathsAt(base, pathspecs, root)));
108
138
  const clones = yield* scan(root, head, held);
109
139
  const after = repeatedLines(clones);
110
- const rises = held.flatMap((file): Rise[] => {
111
- const was = before.get(formerPath.get(file) ?? file) ?? 0;
112
- const is = after.get(file) ?? 0;
113
- return is > was ? [{ file, before: was, after: is, clones: clonesOf(file, clones) }] : [];
114
- });
115
- const risen = new Set(rises.map((rise) => rise.file));
116
- const advisory = [...after].filter(([file]) => !risen.has(file)).concat([...repeatedLines(yield* scan(root, head, others))]);
117
- return {
118
- measured: held.length,
119
- rises: rises.toSorted((a, b) => a.file.localeCompare(b.file)),
120
- advisory: new Map(advisory.toSorted(([a], [b]) => a.localeCompare(b))),
121
- } satisfies Held;
140
+ const rises = risesOf(held, before, after, formerPath, clones);
141
+ const othersRepeated = repeatedLines(yield* scan(root, head, others));
142
+ return heldOf(held.length, rises, after, othersRepeated) satisfies Held;
122
143
  });
123
144
 
124
- function describe({ file, before, after, clones }: Rise): readonly string[] {
145
+ export function describe({ file, before, after, clones }: Rise): readonly string[] {
125
146
  return [
126
147
  ` ${file}: ${after} repeated line(s), up from ${before}`,
127
148
  ...clones.map(([own, other]) => ` ${own.file}:${own.start}-${own.end} repeats ${other.file}:${other.start}-${other.end}`),
128
149
  ];
129
150
  }
130
151
 
131
- function report({ measured, rises, advisory }: Held): string {
152
+ export function report({ measured, rises, advisory }: Held): string {
132
153
  const verdict =
133
154
  rises.length === 0
134
155
  ? [`${NAME}: ${measured} production file(s) repeat no more lines than where the range starts, ${MEASURE}`]
@@ -20,9 +20,9 @@ import {
20
20
  type SizeRule,
21
21
  } from "./size-rules.ts";
22
22
 
23
- type Held = { readonly path: string; readonly from: string };
23
+ export type Held = { readonly path: string; readonly from: string };
24
24
 
25
- type Site = {
25
+ export type Site = {
26
26
  readonly file: string;
27
27
  readonly line: number | undefined;
28
28
  readonly rule: SizeRule["rule"];
@@ -30,7 +30,7 @@ type Site = {
30
30
  readonly message: string;
31
31
  };
32
32
 
33
- type Growth = {
33
+ export type Growth = {
34
34
  readonly file: string;
35
35
  readonly rule: SizeRule["rule"];
36
36
  readonly base: number;
@@ -38,7 +38,7 @@ type Growth = {
38
38
  readonly sites: readonly Site[];
39
39
  };
40
40
 
41
- type Verdict =
41
+ export type Verdict =
42
42
  | { readonly applies: "all"; readonly held: number; readonly overruns: readonly Site[]; readonly advisory: readonly Site[] }
43
43
  | { readonly applies: "ratchet"; readonly held: number; readonly growths: readonly Growth[]; readonly advisory: readonly Site[] };
44
44
 
@@ -69,7 +69,17 @@ const Diagnostic = Schema.Struct({
69
69
 
70
70
  const decodeReport = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Struct({ diagnostics: Schema.Array(Diagnostic) })));
71
71
 
72
- function rulesOf(budget: Budget): Record<string, unknown> {
72
+ type Rules = Readonly<Record<string, unknown>>;
73
+
74
+ export type SizeConfig = {
75
+ readonly plugins: readonly string[];
76
+ readonly jsPlugins: readonly string[];
77
+ readonly categories: Readonly<Record<string, string>>;
78
+ readonly rules: Rules;
79
+ readonly overrides: readonly { readonly files: readonly string[]; readonly rules: Rules }[];
80
+ };
81
+
82
+ function rulesOf(budget: Budget): Rules {
73
83
  return Object.fromEntries(
74
84
  SIZE_RULES.map((entry) => {
75
85
  const max = budget[entry.key];
@@ -78,7 +88,7 @@ function rulesOf(budget: Budget): Record<string, unknown> {
78
88
  );
79
89
  }
80
90
 
81
- function sizeConfig({ production, tests }: Budgets, plugin: string): unknown {
91
+ export function sizeConfig({ production, tests }: Budgets, plugin: string): SizeConfig {
82
92
  return {
83
93
  plugins: [],
84
94
  jsPlugins: [plugin],
@@ -88,7 +98,7 @@ function sizeConfig({ production, tests }: Budgets, plugin: string): unknown {
88
98
  };
89
99
  }
90
100
 
91
- function heldByChange(changes: readonly Change[]): readonly Held[] {
101
+ export function heldByChange(changes: readonly Change[]): readonly Held[] {
92
102
  return changes.flatMap((change) => {
93
103
  if (change.kind === "written") return [{ path: change.path, from: change.path }];
94
104
  if (change.kind === "renamed" && change.edited) return [{ path: change.path, from: change.from }];
@@ -118,7 +128,9 @@ const materializeBase = Effect.fn("materializeBase")(function* (root: string, ba
118
128
  yield* git(["checkout-index", "--all", `--prefix=${tree}/`], root, { env });
119
129
  });
120
130
 
121
- const siteOf = (budgets: Budgets) =>
131
+ export const siteOf = (
132
+ budgets: Budgets,
133
+ ): ((diagnostic: typeof Diagnostic.Type) => Effect.Effect<readonly Site[], OxlintUnreadable>) =>
122
134
  Effect.fn("siteOf")(function* ({ code, message, filename, labels }: typeof Diagnostic.Type) {
123
135
  const rule = SIZE_RULES.find((candidate) => code === diagnosticCode(candidate));
124
136
  if (rule === undefined) return [];
@@ -166,7 +178,8 @@ function total(sites: readonly Site[]): number {
166
178
  return sites.reduce((sum, { overrun }) => sum + overrun, 0);
167
179
  }
168
180
 
169
- function growthsOf(head: readonly Site[], base: readonly Site[]): readonly Growth[] {
181
+ // Sums each rule per file separately, so a shrink in one rule or file never offsets a growth in another.
182
+ export function growthsOf(head: readonly Site[], base: readonly Site[]): readonly Growth[] {
170
183
  const before = byFileAndRule(base);
171
184
  return [...byFileAndRule(head).entries()].flatMap(([group, sites]) => {
172
185
  const [first] = sites;
@@ -175,6 +188,24 @@ function growthsOf(head: readonly Site[], base: readonly Site[]): readonly Growt
175
188
  });
176
189
  }
177
190
 
191
+ // Decides the verdict from sites oxlint already measured: which are held to the range or the
192
+ // whole tree, and, under ratchet, which grew past what the base measured for the same file and rule.
193
+ export function verdictOf(
194
+ applies: Applies,
195
+ held: number,
196
+ holds: ReadonlySet<string>,
197
+ sites: readonly Site[],
198
+ baseSites: readonly Site[],
199
+ ): Verdict {
200
+ const heldSites = sites.filter((site) => holds.has(site.file));
201
+ if (applies !== "ratchet") {
202
+ return { applies, held, overruns: heldSites, advisory: sites.filter((site) => !holds.has(site.file)) };
203
+ }
204
+ const growths = growthsOf(heldSites, baseSites);
205
+ const failing = new Set(growths.flatMap((growth) => growth.sites));
206
+ return { applies, held, growths, advisory: sites.filter((site) => !failing.has(site)) };
207
+ }
208
+
178
209
  const heldFiles = Effect.fn("heldFiles")(function* (root: string, applies: Applies, pathspecs: readonly string[], base: string, head: string) {
179
210
  if (applies === "all") return (yield* pathsAt(head, pathspecs, root)).map((path): Held => ({ path, from: path }));
180
211
  return heldByChange(yield* changedPaths(base, head, pathspecs, root));
@@ -196,25 +227,20 @@ const runBudget = Effect.fn("runBudget")(
196
227
  if (held.length + others.length > 0) yield* materializeHead(root, head, [...holds, ...others], headTree);
197
228
  const plugin = path.join(import.meta.dir, "..", "dist", "index.js");
198
229
  const sites = yield* measure(headTree, budgets, plugin);
199
- const heldSites = sites.filter((site) => holds.has(site.file));
200
- if (applies !== "ratchet") {
201
- return { applies, held: held.length, overruns: heldSites, advisory: sites.filter((site) => !holds.has(site.file)) } satisfies Verdict;
202
- }
230
+ if (applies !== "ratchet") return verdictOf(applies, held.length, holds, sites, []);
203
231
 
204
232
  const baseTree = path.join(scratch, "base");
205
233
  if (held.length > 0) yield* materializeBase(root, base, held, baseTree);
206
- const growths = growthsOf(heldSites, yield* measure(baseTree, budgets, plugin));
207
- const failing = new Set(growths.flatMap((growth) => growth.sites));
208
- return { applies, held: held.length, growths, advisory: sites.filter((site) => !failing.has(site)) } satisfies Verdict;
234
+ return verdictOf(applies, held.length, holds, sites, yield* measure(baseTree, budgets, plugin));
209
235
  },
210
236
  Effect.scoped,
211
237
  );
212
238
 
213
- function describe({ file, line, message }: Site, indent = " "): string {
239
+ export function describe({ file, line, message }: Site, indent = " "): string {
214
240
  return `${indent}${file}${line === undefined ? "" : `:${line}`}: ${message}`;
215
241
  }
216
242
 
217
- function verdictLines(verdict: Verdict): readonly string[] {
243
+ export function verdictLines(verdict: Verdict): readonly string[] {
218
244
  const scope = SCOPE[verdict.applies];
219
245
  if (verdict.applies === "ratchet") {
220
246
  if (verdict.growths.length === 0) return [`${NAME}: ${verdict.held} file(s), ${scope}, raise no overrun past the base`];
@@ -230,7 +256,7 @@ function verdictLines(verdict: Verdict): readonly string[] {
230
256
  return [`${NAME}: ${verdict.overruns.length} overrun(s) of the budget in ${scope}:`, ...verdict.overruns.map((site) => describe(site))];
231
257
  }
232
258
 
233
- function report(verdict: Verdict): string {
259
+ export function report(verdict: Verdict): string {
234
260
  const { advisory } = verdict;
235
261
  const notice =
236
262
  advisory.length === 0
@@ -239,7 +265,7 @@ function report(verdict: Verdict): string {
239
265
  return [...verdictLines(verdict), ...notice].join("\n");
240
266
  }
241
267
 
242
- function passes(verdict: Verdict): boolean {
268
+ export function passes(verdict: Verdict): boolean {
243
269
  return verdict.applies === "ratchet" ? verdict.growths.length === 0 : verdict.overruns.length === 0;
244
270
  }
245
271
 
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  import { Console, Effect, Schema } from "effect";
3
- import { git, parentOrEmptyTree, refArgs } from "./git.ts";
3
+ import { git, commitOf, parentOrEmptyTree, refArgs } from "./git.ts";
4
4
  import { runMain } from "./main.ts";
5
5
 
6
6
  export const SUPPRESSIONS = "oxlint-suppressions.json";
@@ -87,10 +87,6 @@ export function report({ counted, lowered, rises }: Ratchet): string {
87
87
  ].join("\n");
88
88
  }
89
89
 
90
- const commitOf = Effect.fn("commitOf")(function* (rev: string) {
91
- return (yield* git(["rev-parse", "--verify", `${rev}^{commit}`])).trim();
92
- });
93
-
94
90
  const mergeBase = Effect.fn("mergeBase")(function* (base: string, head: string) {
95
91
  const commit = yield* git(["merge-base", base, head]).pipe(
96
92
  Effect.mapError(() => new SuppressionsError({ message: `${base} and ${head} share no commit` })),
@@ -3,7 +3,8 @@ import { parse } from "@swc/core";
3
3
  import { Console, Effect, FileSystem, Path, Schema } from "effect";
4
4
  import { git } from "./git.ts";
5
5
  import { runMain } from "./main.ts";
6
- import { ENTRY_POINT, TEST_ENTRY_POINT } from "./gates.ts";
6
+ import { ENTRY_POINT, QUALITY_FILE, TEST_ENTRY_POINT } from "./gates.ts";
7
+ import { readQuality } from "./quality-file.ts";
7
8
 
8
9
  export type Violation = {
9
10
  readonly file: string;
@@ -16,7 +17,7 @@ const E2E = "tests/e2e/";
16
17
  const HELPER_DIRS = ["tests/lib/", "tests/fixtures/"] as const;
17
18
  const DATA_DIR = "tests/fixtures/";
18
19
 
19
- const TEST_FILE = /(?:[.](?:test|spec)|_test)[.](tsx?)$/;
20
+ export const TEST_FILE = /(?:[.](?:test|spec)|_test)[.](tsx?)$/;
20
21
  const TARGET_FILE = /^tests\/(?:[^/]+\/)*[^/]+[.]test[.]tsx?$/;
21
22
  const TYPESCRIPT = /[.]tsx?$/;
22
23
 
@@ -39,9 +40,14 @@ const BANNED_BUN_NAMES: readonly string[] = [
39
40
  ];
40
41
  const BANNED_GLOBAL_CALLS: readonly string[] = ["fetch"];
41
42
 
42
- const REQUIRED_TEST_TABLE: Record<string, unknown> = {
43
- pathIgnorePatterns: ["**/tests/quarantine/**"],
44
- };
43
+ const IGNORES_KEY = "pathIgnorePatterns";
44
+ const QUARANTINE = "**/tests/quarantine/**";
45
+ const VENDORED = "repos/**";
46
+ const PRESET_IGNORES: readonly string[] = [QUARANTINE, VENDORED];
47
+
48
+ function acceptedIgnores(vendors: boolean): readonly (readonly string[])[] {
49
+ return vendors ? [PRESET_IGNORES] : [PRESET_IGNORES, [QUARANTINE]];
50
+ }
45
51
  export const LAYOUT_CHECK_MARK = "scripts/test-layout.ts";
46
52
  export const LAYOUT_CHECK_BIN = "checks-test-layout";
47
53
  const OWN_ENTRY_POINT = `scripts/${ENTRY_POINT.script}`;
@@ -245,7 +251,7 @@ export function scriptViolations(manifest: unknown): readonly Violation[] {
245
251
  return violations;
246
252
  }
247
253
 
248
- export function bunfigViolations(consumer: unknown, preset: unknown): readonly Violation[] {
254
+ export function bunfigViolations(consumer: unknown, preset: unknown, vendors: boolean): readonly Violation[] {
249
255
  const file = "bunfig.toml";
250
256
  const copy = "bun has no bunfig extends, so copy node_modules/@avi2dg/checks/bunfig.toml";
251
257
  if (consumer === undefined) {
@@ -255,18 +261,21 @@ export function bunfigViolations(consumer: unknown, preset: unknown): readonly V
255
261
  if (!isRecord(presetTest)) {
256
262
  return [{ file, line: undefined, message: "the shipped bunfig preset has no [test] table" }];
257
263
  }
258
- const expected = { ...presetTest, ...REQUIRED_TEST_TABLE };
259
264
  const consumerTest = isRecord(consumer) ? consumer["test"] : undefined;
265
+ const found = (key: string): unknown => (isRecord(consumerTest) ? consumerTest[key] : undefined);
266
+ const drifted = (key: string, wanted: string, fix: string): Violation => ({
267
+ file,
268
+ line: undefined,
269
+ message: `[test].${key} must be ${wanted}, found ${JSON.stringify(found(key) ?? null)}; ${fix}`,
270
+ });
260
271
  const violations: Violation[] = [];
261
- for (const [key, value] of Object.entries(expected)) {
262
- const found = isRecord(consumerTest) ? consumerTest[key] : undefined;
263
- if (!Bun.deepEquals(found, value)) {
264
- violations.push({
265
- file,
266
- line: undefined,
267
- message: `[test].${key} must be ${JSON.stringify(value)}, found ${JSON.stringify(found ?? null)}; ${copy}`,
268
- });
269
- }
272
+ for (const [key, value] of Object.entries(presetTest)) {
273
+ if (key !== IGNORES_KEY && !Bun.deepEquals(found(key), value)) violations.push(drifted(key, JSON.stringify(value), copy));
274
+ }
275
+ const accepted = acceptedIgnores(vendors);
276
+ if (!accepted.some((ignores) => Bun.deepEquals(found(IGNORES_KEY), ignores))) {
277
+ const wanted = accepted.map((ignores) => JSON.stringify(ignores)).join(" or ");
278
+ violations.push(drifted(IGNORES_KEY, wanted, `the check pins it, requiring ${VENDORED} where ${QUALITY_FILE} declares sources.libraries`));
270
279
  }
271
280
  return violations;
272
281
  }
@@ -321,7 +330,9 @@ export const run = Effect.fn("run")(function* (root: string, presetPath: string)
321
330
 
322
331
  const bunfig = path.join(root, "bunfig.toml");
323
332
  const consumerBunfig = (yield* fs.exists(bunfig)) ? yield* parsedToml(bunfig) : undefined;
324
- violations.push(...bunfigViolations(consumerBunfig, yield* parsedToml(presetPath)));
333
+ const { quality } = yield* readQuality(root);
334
+ const vendors = (quality.sources?.libraries ?? []).length > 0;
335
+ violations.push(...bunfigViolations(consumerBunfig, yield* parsedToml(presetPath), vendors));
325
336
 
326
337
  return { files: files.length, violations };
327
338
  });