@avi2dg/checks 0.21.0 → 0.23.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 +66 -40
- package/CONTRIBUTING.md +14 -10
- package/README.md +15 -24
- package/bunfig.toml +1 -1
- package/dist/effect-channel/index.js +122 -0
- package/dist/{index.js → readability/index.js} +8 -120
- package/docs/configs/commit-messages.md +5 -1
- package/docs/configs/dependency-rules.md +5 -2
- package/docs/configs/effect-rules.md +32 -33
- package/docs/configs/native-settings.md +74 -0
- package/docs/configs/typescript-rules.md +4 -0
- package/docs/design.md +26 -27
- package/docs/gates/checks-backtest.md +4 -0
- package/docs/gates/checks-ci-wiring.md +30 -93
- package/docs/gates/checks-comment-gate.md +4 -0
- package/docs/gates/checks-commit-identity.md +23 -27
- package/docs/gates/checks-docs.md +23 -21
- package/docs/gates/checks-flake.md +4 -10
- package/docs/gates/checks-lint-coverage.md +5 -1
- package/docs/gates/checks-lint.md +22 -104
- package/docs/gates/checks-mutation-compare.md +4 -0
- package/docs/gates/checks-quarantine-clock.md +4 -0
- package/docs/gates/checks-repetition.md +27 -41
- package/docs/gates/checks-subsumed-tests.md +4 -0
- package/docs/gates/checks-suppressions-ratchet.md +4 -0
- package/docs/gates/checks-test-layout.md +16 -8
- package/docs/gates/checks-test.md +68 -36
- package/docs/gates/checks-vendor.md +20 -13
- package/oxlintrc.json +1 -1
- package/package.json +10 -21
- package/scripts/ci-wiring.ts +28 -97
- package/scripts/commit-identity.ts +32 -4
- package/scripts/doc-rules.ts +26 -11
- package/scripts/doc-templates.ts +2 -1
- package/scripts/docs.ts +4 -7
- package/scripts/gates.ts +0 -29
- package/scripts/git.ts +24 -1
- package/scripts/lint.ts +15 -34
- package/scripts/range-gate.ts +1 -2
- package/scripts/repetition.ts +51 -40
- package/scripts/shell-command.ts +7 -1
- package/scripts/swc.ts +46 -0
- package/scripts/test-layout.ts +40 -56
- package/scripts/test-skips.ts +180 -0
- package/scripts/test.ts +33 -38
- package/scripts/vendor.ts +55 -11
- package/dist/feature-rules.js +0 -354
- package/docs/configs/quality-file.md +0 -103
- package/docs/gates/checks-feature-owners.md +0 -113
- package/docs/gates/checks-quality.md +0 -111
- package/docs/gates/checks-size-budget.md +0 -107
- package/quality.schema.json +0 -514
- package/scripts/feature-owners.ts +0 -139
- package/scripts/quality-file.ts +0 -353
- package/scripts/quality.ts +0 -363
- package/scripts/size-budget.ts +0 -285
- package/scripts/size-rules.ts +0 -126
package/scripts/test.ts
CHANGED
|
@@ -3,20 +3,10 @@ import { Config, Console, Effect, FileSystem, Path, Schema } from "effect";
|
|
|
3
3
|
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
|
|
4
4
|
import { TEST_ENTRY_POINT } from "./gates.ts";
|
|
5
5
|
import { runMain, Usage } from "./main.ts";
|
|
6
|
+
import { readSkipDeclarations, type Environment, type SkipDeclaration, type TestTier } from "./test-skips.ts";
|
|
6
7
|
import { NAME_SEPARATOR, parseReport, ReportError, reporterArgs, type TestResult } from "./test-report.ts";
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export type Environment = typeof Environment.Type;
|
|
11
|
-
|
|
12
|
-
const SkipDeclaration = Schema.Struct({
|
|
13
|
-
file: Schema.NonEmptyString,
|
|
14
|
-
test: Schema.NonEmptyString,
|
|
15
|
-
reason: Schema.NonEmptyString,
|
|
16
|
-
when: Schema.optionalKey(Environment),
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
export type SkipDeclaration = typeof SkipDeclaration.Type;
|
|
9
|
+
export type { Environment, SkipDeclaration } from "./test-skips.ts";
|
|
20
10
|
|
|
21
11
|
export type Verdict = {
|
|
22
12
|
readonly environment: Environment;
|
|
@@ -31,15 +21,23 @@ export class SkipGateError extends Schema.TaggedError<SkipGateError>()("SkipGate
|
|
|
31
21
|
}) {}
|
|
32
22
|
|
|
33
23
|
const NAME = TEST_ENTRY_POINT.bin;
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
);
|
|
24
|
+
const USAGE = `usage: ${NAME} takes no arguments except --tier=live or --tier=pixel`;
|
|
25
|
+
const TIERS = new Map<string, TestTier>([
|
|
26
|
+
["--tier=live", "live"],
|
|
27
|
+
["--tier=pixel", "pixel"],
|
|
28
|
+
]);
|
|
40
29
|
|
|
41
30
|
function matches(declaration: SkipDeclaration, result: TestResult): boolean {
|
|
42
|
-
|
|
31
|
+
const { name } = declaration;
|
|
32
|
+
return (
|
|
33
|
+
declaration.file === result.file &&
|
|
34
|
+
declaration.line === result.line &&
|
|
35
|
+
(result.name === name || result.name.endsWith(`${NAME_SEPARATOR}${name}`))
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function skippedOutcome(result: TestResult): boolean {
|
|
40
|
+
return result.outcome === "skipped" || result.outcome === "todo";
|
|
43
41
|
}
|
|
44
42
|
|
|
45
43
|
function byPlace(a: TestResult, b: TestResult): number {
|
|
@@ -51,7 +49,7 @@ export function judgeSkips(
|
|
|
51
49
|
declarations: readonly SkipDeclaration[],
|
|
52
50
|
environment: Environment,
|
|
53
51
|
): Verdict {
|
|
54
|
-
const skipped = results.filter(
|
|
52
|
+
const skipped = results.filter(skippedOutcome);
|
|
55
53
|
const applying = declarations.filter((declaration) => (declaration.when ?? environment) === environment);
|
|
56
54
|
return {
|
|
57
55
|
environment,
|
|
@@ -71,7 +69,7 @@ export function passes(verdict: Verdict): boolean {
|
|
|
71
69
|
}
|
|
72
70
|
|
|
73
71
|
function staleLine(declaration: SkipDeclaration): string {
|
|
74
|
-
return ` ${declaration.file}
|
|
72
|
+
return ` ${declaration.file}:${declaration.line}: no test skipped with this declaration; reason: ${declaration.reason}`;
|
|
75
73
|
}
|
|
76
74
|
|
|
77
75
|
function verdictLines(verdict: Verdict): readonly string[] {
|
|
@@ -80,15 +78,18 @@ function verdictLines(verdict: Verdict): readonly string[] {
|
|
|
80
78
|
if (passes(verdict)) {
|
|
81
79
|
const other = environment === "ci" ? "local" : "ci";
|
|
82
80
|
const aside = unjudged === 0 ? "" : `; ${unjudged} declaration(s) for ${other} not judged in ${run}`;
|
|
83
|
-
return [skipped === 0 ? `${NAME}: no test skipped${aside}` : `${NAME}: ${skipped} skipped test(s), each declared
|
|
81
|
+
return [skipped === 0 ? `${NAME}: no test skipped${aside}` : `${NAME}: ${skipped} skipped test(s), each declared at its test site${aside}`];
|
|
84
82
|
}
|
|
85
83
|
const stale = refusedStale(verdict);
|
|
86
84
|
const counted = environment === "ci" ? ` and ${stale.length} declaration(s) matching no skipped test` : "";
|
|
87
85
|
return [
|
|
88
86
|
`${NAME}: ${undeclared.length} skipped test(s) undeclared${counted} in ${run}:`,
|
|
89
87
|
...undeclared.map((result) => {
|
|
90
|
-
const kind =
|
|
91
|
-
|
|
88
|
+
const [kind, usage] =
|
|
89
|
+
result.outcome === "todo"
|
|
90
|
+
? ["a todo", "test.todo(skipReason(reason, name))"]
|
|
91
|
+
: ["skipped", "test.skipIf(condition)(skipReason(reason, name), fn)"];
|
|
92
|
+
return ` ${result.file}:${result.line} ${result.name}: ${kind} with no reason at its test site; use ${usage}`;
|
|
92
93
|
}),
|
|
93
94
|
...stale.map(staleLine),
|
|
94
95
|
];
|
|
@@ -106,37 +107,31 @@ export function report(verdict: Verdict): string {
|
|
|
106
107
|
return [...verdictLines(verdict), ...warning].join("\n");
|
|
107
108
|
}
|
|
108
109
|
|
|
109
|
-
const readDeclarations = Effect.fn("readDeclarations")(function* (root: string) {
|
|
110
|
-
const manifest = (yield* Path.Path).join(root, "package.json");
|
|
111
|
-
const { testSkips } = yield* (yield* FileSystem.FileSystem).readFileString(manifest).pipe(
|
|
112
|
-
Effect.flatMap(decodeManifest),
|
|
113
|
-
Effect.mapError((cause) => new SkipGateError({ message: `cannot read ${DECLARATIONS}: ${cause.message}` })),
|
|
114
|
-
);
|
|
115
|
-
return testSkips ?? [];
|
|
116
|
-
});
|
|
117
|
-
|
|
118
110
|
const environmentOf = Config.Boolean("CI").pipe(
|
|
119
111
|
Config.withDefault(false),
|
|
120
112
|
Config.map((ci): Environment => (ci ? "ci" : "local")),
|
|
121
113
|
Effect.mapError((cause) => new SkipGateError({ message: `cannot tell a ci run from a local one: ${cause.message}` })),
|
|
122
114
|
);
|
|
123
115
|
|
|
124
|
-
const runSuite = Effect.fn("runSuite")(function* (outfile: string) {
|
|
116
|
+
const runSuite = Effect.fn("runSuite")(function* (outfile: string, tier: TestTier | undefined) {
|
|
125
117
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
126
|
-
const
|
|
118
|
+
const tierArgs = tier === undefined ? [] : ["--path-ignore-patterns", "", `./tests/${tier}`];
|
|
119
|
+
const args = ["test", "--randomize", ...tierArgs, ...reporterArgs(outfile)];
|
|
127
120
|
return yield* spawner.exitCode(
|
|
128
121
|
ChildProcess.make(process.execPath, args, { stdin: "ignore", stdout: "inherit", stderr: "inherit" }),
|
|
129
122
|
);
|
|
130
123
|
});
|
|
131
124
|
|
|
132
125
|
const testEntry = Effect.gen(function* () {
|
|
133
|
-
|
|
126
|
+
const args = process.argv.slice(2);
|
|
127
|
+
const tier = args.length === 0 ? undefined : args.length === 1 ? TIERS.get(args[0] ?? "") : undefined;
|
|
128
|
+
if (args.length > 0 && tier === undefined) return yield* new Usage({ message: USAGE });
|
|
134
129
|
const fs = yield* FileSystem.FileSystem;
|
|
135
|
-
const declarations = yield*
|
|
130
|
+
const declarations = yield* readSkipDeclarations(process.cwd(), tier);
|
|
136
131
|
const environment = yield* environmentOf;
|
|
137
132
|
|
|
138
133
|
const outfile = (yield* Path.Path).join(yield* fs.makeTempDirectoryScoped({ prefix: "checks-test-" }), "junit.xml");
|
|
139
|
-
const suitePassed = (yield* runSuite(outfile)) === ChildProcessSpawner.ExitCode(0);
|
|
134
|
+
const suitePassed = (yield* runSuite(outfile, tier)) === ChildProcessSpawner.ExitCode(0);
|
|
140
135
|
if (!(yield* fs.exists(outfile))) {
|
|
141
136
|
if (!suitePassed) {
|
|
142
137
|
yield* Console.error(`${NAME}: bun test failed before it wrote a report, so no skip was judged`);
|
package/scripts/vendor.ts
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { Console, Effect, FileSystem, Option, Path, Schema } from "effect";
|
|
3
|
-
import { QUALITY_FILE } from "./gates.ts";
|
|
4
3
|
import { git } from "./git.ts";
|
|
5
4
|
import { runMain, Usage } from "./main.ts";
|
|
6
|
-
import { readQuality, type Library } from "./quality-file.ts";
|
|
7
5
|
|
|
8
6
|
const NAME = "checks-vendor";
|
|
9
|
-
const USAGE = `usage: ${NAME}
|
|
7
|
+
const USAGE = `usage: ${NAME} [--library <name> --package <package> --repository <remote> --tag <template> [--path <manifest>]]...`;
|
|
10
8
|
const CACHE_HOME = ".cache/avi2dg-checks";
|
|
11
9
|
const RECORD_SUFFIX = ".commit";
|
|
12
10
|
const LINKS = "repos";
|
|
@@ -23,6 +21,55 @@ export class Unreachable extends Schema.TaggedError<Unreachable>()("Unreachable"
|
|
|
23
21
|
message: Schema.String,
|
|
24
22
|
}) {}
|
|
25
23
|
|
|
24
|
+
const Library = Schema.Struct({
|
|
25
|
+
name: Schema.String.check(Schema.isPattern(/^[a-z0-9]+(?:-[a-z0-9]+)*$/)),
|
|
26
|
+
package: Schema.NonEmptyString,
|
|
27
|
+
repository: Schema.NonEmptyString,
|
|
28
|
+
tag: Schema.String.check(Schema.isPattern(/\{version\}/)),
|
|
29
|
+
path: Schema.optionalKey(Schema.String.check(Schema.isPattern(/^(?:[\w.@+-]+\/)*[\w.@+-]+\.\w+$/))),
|
|
30
|
+
});
|
|
31
|
+
type Library = typeof Library.Type;
|
|
32
|
+
|
|
33
|
+
const Libraries = Schema.Array(Library).check(
|
|
34
|
+
Schema.makeFilter((libraries) => {
|
|
35
|
+
const names = libraries.map(({ name }) => name);
|
|
36
|
+
const repeated = names.find((name, index) => names.indexOf(name) !== index);
|
|
37
|
+
return repeated === undefined || `--library ${repeated} appears more than once`;
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const OPENER = "--library";
|
|
42
|
+
const FIELDS = new Map<string, keyof Library>([
|
|
43
|
+
["--package", "package"],
|
|
44
|
+
["--repository", "repository"],
|
|
45
|
+
["--tag", "tag"],
|
|
46
|
+
["--path", "path"],
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
function misuse(message: string): Usage {
|
|
50
|
+
return new Usage({ message: `${message}\n${USAGE}` });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const librariesFrom = Effect.fn("librariesFrom")(function* (args: readonly string[]) {
|
|
54
|
+
const groups: Record<string, string>[] = [];
|
|
55
|
+
for (let index = 0; index < args.length; index += 2) {
|
|
56
|
+
const flag = args[index] ?? "";
|
|
57
|
+
const value = args[index + 1];
|
|
58
|
+
if (value === undefined || value.startsWith("--")) return yield* misuse(`${flag} takes a value`);
|
|
59
|
+
if (flag === OPENER) {
|
|
60
|
+
groups.push({ name: value });
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const field = FIELDS.get(flag);
|
|
64
|
+
const current = groups.at(-1);
|
|
65
|
+
if (field === undefined) return yield* misuse(`${flag} is not an argument`);
|
|
66
|
+
if (current === undefined) return yield* misuse(`${flag} comes before any ${OPENER}`);
|
|
67
|
+
if (field in current) return yield* misuse(`${flag} appears twice for ${OPENER} ${current["name"]}`);
|
|
68
|
+
current[field] = value;
|
|
69
|
+
}
|
|
70
|
+
return yield* Schema.decodeUnknownEffect(Libraries)(groups).pipe(Effect.mapError((cause) => misuse(cause.message)));
|
|
71
|
+
});
|
|
72
|
+
|
|
26
73
|
export function tagFor(template: string, version: string): string {
|
|
27
74
|
return template.replaceAll(VERSION_TOKEN, version);
|
|
28
75
|
}
|
|
@@ -296,8 +343,11 @@ const cacheRoot = Effect.fn("cacheRoot")(function* () {
|
|
|
296
343
|
});
|
|
297
344
|
|
|
298
345
|
const main = Effect.gen(function* () {
|
|
299
|
-
const
|
|
300
|
-
if (
|
|
346
|
+
const libraries = yield* librariesFrom(process.argv.slice(2));
|
|
347
|
+
if (libraries.length === 0) {
|
|
348
|
+
yield* Console.error(`${NAME}: no ${OPENER} argument names a library, so nothing is pinned`);
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
301
351
|
const toplevel = yield* git(["rev-parse", "--show-toplevel"]).pipe(
|
|
302
352
|
Effect.map((output) => Option.some(output.trim())),
|
|
303
353
|
Effect.catchTag("GitFailure", (failure) =>
|
|
@@ -308,12 +358,6 @@ const main = Effect.gen(function* () {
|
|
|
308
358
|
);
|
|
309
359
|
if (Option.isNone(toplevel)) return true;
|
|
310
360
|
const root = toplevel.value;
|
|
311
|
-
const { quality } = yield* readQuality(root);
|
|
312
|
-
const libraries = quality.sources?.libraries ?? [];
|
|
313
|
-
if (libraries.length === 0) {
|
|
314
|
-
yield* Console.error(`${NAME}: ${QUALITY_FILE} declares no libraries, so nothing is pinned`);
|
|
315
|
-
return true;
|
|
316
|
-
}
|
|
317
361
|
const cache = yield* cacheRoot();
|
|
318
362
|
let passed = true;
|
|
319
363
|
for (const library of libraries) {
|
package/dist/feature-rules.js
DELETED
|
@@ -1,354 +0,0 @@
|
|
|
1
|
-
// scripts/feature-rules.ts
|
|
2
|
-
import { Schema as Schema4 } from "effect";
|
|
3
|
-
|
|
4
|
-
// scripts/quality-file.ts
|
|
5
|
-
import { Console, Effect, FileSystem, JsonSchema, Path, Schema as Schema3 } from "effect";
|
|
6
|
-
|
|
7
|
-
// scripts/gates.ts
|
|
8
|
-
import { Schema } from "effect";
|
|
9
|
-
var EVERY_REPOSITORY = "every repository";
|
|
10
|
-
var QUALITY_FILE = "quality.json";
|
|
11
|
-
var TYPESCRIPT_SOURCE = { pathspecs: ["*.ts", "*.tsx"], content: "TypeScript source" };
|
|
12
|
-
var QUALITY_DECLARATION = { pathspecs: [QUALITY_FILE], content: `a ${QUALITY_FILE}` };
|
|
13
|
-
var KIT_GATES = [
|
|
14
|
-
{ bin: "checks-lint-coverage", script: "lint-coverage.sh", reads: "tree", appliesTo: TYPESCRIPT_SOURCE },
|
|
15
|
-
{ bin: "checks-test-layout", script: "test-layout.ts", reads: "tree", appliesTo: TYPESCRIPT_SOURCE },
|
|
16
|
-
{ bin: "checks-commit-identity", script: "commit-identity.ts", reads: "range", appliesTo: EVERY_REPOSITORY },
|
|
17
|
-
{ bin: "checks-comment-gate", script: "comment-gate.ts", reads: "range", appliesTo: EVERY_REPOSITORY },
|
|
18
|
-
{ bin: "checks-suppressions-ratchet", script: "suppressions-ratchet.ts", reads: "range", appliesTo: EVERY_REPOSITORY },
|
|
19
|
-
{ bin: "checks-ci-wiring", script: "ci-wiring.ts", reads: "tree", appliesTo: EVERY_REPOSITORY },
|
|
20
|
-
{ bin: "checks-docs", script: "docs.ts", reads: "range", appliesTo: EVERY_REPOSITORY },
|
|
21
|
-
{ bin: "checks-quality", script: "quality.ts", reads: "tree", args: ["--check"], appliesTo: QUALITY_DECLARATION },
|
|
22
|
-
{ bin: "checks-size-budget", script: "size-budget.ts", reads: "range", appliesTo: TYPESCRIPT_SOURCE },
|
|
23
|
-
{ bin: "checks-repetition", script: "repetition.ts", reads: "range", appliesTo: TYPESCRIPT_SOURCE },
|
|
24
|
-
{ bin: "checks-feature-owners", script: "feature-owners.ts", reads: "range", appliesTo: TYPESCRIPT_SOURCE },
|
|
25
|
-
{ bin: "checks-quarantine-clock", script: "quarantine-clock.ts", reads: "range", appliesTo: EVERY_REPOSITORY }
|
|
26
|
-
];
|
|
27
|
-
var UNCONDITIONAL = KIT_GATES.filter((gate) => gate.appliesTo === EVERY_REPOSITORY).map((gate) => gate.bin);
|
|
28
|
-
var LintGates = Schema.Array(Schema.Literals(KIT_GATES.map((gate) => gate.bin))).check(Schema.makeFilter((selected) => {
|
|
29
|
-
const missing = UNCONDITIONAL.filter((bin) => !selected.includes(bin));
|
|
30
|
-
if (missing.length === 0)
|
|
31
|
-
return true;
|
|
32
|
-
const verb = missing.length === 1 ? "applies" : "apply";
|
|
33
|
-
return `checks-lint must run ${missing.join(", ")}, which ${verb} to ${EVERY_REPOSITORY}`;
|
|
34
|
-
}, { toJsonSchema: () => ({ allOf: UNCONDITIONAL.map((bin) => ({ contains: { const: bin } })) }) }));
|
|
35
|
-
|
|
36
|
-
// scripts/size-rules.ts
|
|
37
|
-
import { Schema as Schema2 } from "effect";
|
|
38
|
-
var TESTS_DIRECTORY = "tests";
|
|
39
|
-
var COUNTED = { skipBlankLines: false, skipComments: false };
|
|
40
|
-
var FILE_LINES = {
|
|
41
|
-
key: "fileLines",
|
|
42
|
-
rule: "max-lines",
|
|
43
|
-
options: COUNTED,
|
|
44
|
-
measured: /has too many lines \((\d+)\)/,
|
|
45
|
-
limits: "The most lines a file may hold, blank and comment lines counted"
|
|
46
|
-
};
|
|
47
|
-
var FUNCTION_LINES = {
|
|
48
|
-
key: "functionLines",
|
|
49
|
-
rule: "max-lines-per-function",
|
|
50
|
-
options: COUNTED,
|
|
51
|
-
measured: /has too many lines \((\d+)\)/,
|
|
52
|
-
limits: "The most lines a function may span, blank and comment lines counted"
|
|
53
|
-
};
|
|
54
|
-
var STATEMENTS = {
|
|
55
|
-
key: "statements",
|
|
56
|
-
rule: "max-statements",
|
|
57
|
-
options: {},
|
|
58
|
-
measured: /has too many statements \((\d+)\)/,
|
|
59
|
-
limits: "The most statements a function may hold"
|
|
60
|
-
};
|
|
61
|
-
var COMPLEXITY = {
|
|
62
|
-
key: "complexity",
|
|
63
|
-
rule: "cognitive-complexity",
|
|
64
|
-
plugin: "effect-channel",
|
|
65
|
-
options: {},
|
|
66
|
-
measured: /has a cognitive complexity of (\d+)/,
|
|
67
|
-
limits: "The highest cognitive complexity a function may reach, a switch counted once"
|
|
68
|
-
};
|
|
69
|
-
var DEPTH = {
|
|
70
|
-
key: "depth",
|
|
71
|
-
rule: "max-depth",
|
|
72
|
-
options: {},
|
|
73
|
-
measured: /nested too deeply \((\d+)\)/,
|
|
74
|
-
limits: "The deepest a block may nest inside a function"
|
|
75
|
-
};
|
|
76
|
-
var APPLIES = ["ratchet", "all"];
|
|
77
|
-
var SIZE_DEFAULTS = {
|
|
78
|
-
applies: "ratchet",
|
|
79
|
-
production: { fileLines: 400, functionLines: 100, statements: 30, complexity: 15, depth: 4 },
|
|
80
|
-
tests: { fileLines: 600, statements: 50, complexity: 15, depth: 4 }
|
|
81
|
-
};
|
|
82
|
-
var Limit = Schema2.Int.check(Schema2.isGreaterThan(0));
|
|
83
|
-
function limit({ limits }, fallback) {
|
|
84
|
-
return Schema2.optionalKey(Limit.annotate({ description: `${limits}; ${fallback} when absent` }));
|
|
85
|
-
}
|
|
86
|
-
var ProductionBudget = Schema2.Struct({
|
|
87
|
-
fileLines: limit(FILE_LINES, SIZE_DEFAULTS.production.fileLines),
|
|
88
|
-
functionLines: limit(FUNCTION_LINES, SIZE_DEFAULTS.production.functionLines),
|
|
89
|
-
statements: limit(STATEMENTS, SIZE_DEFAULTS.production.statements),
|
|
90
|
-
complexity: limit(COMPLEXITY, SIZE_DEFAULTS.production.complexity),
|
|
91
|
-
depth: limit(DEPTH, SIZE_DEFAULTS.production.depth)
|
|
92
|
-
});
|
|
93
|
-
var TestBudget = Schema2.Struct({
|
|
94
|
-
fileLines: limit(FILE_LINES, SIZE_DEFAULTS.tests.fileLines),
|
|
95
|
-
statements: limit(STATEMENTS, SIZE_DEFAULTS.tests.statements),
|
|
96
|
-
complexity: limit(COMPLEXITY, SIZE_DEFAULTS.tests.complexity),
|
|
97
|
-
depth: limit(DEPTH, SIZE_DEFAULTS.tests.depth)
|
|
98
|
-
});
|
|
99
|
-
var Size = Schema2.Struct({
|
|
100
|
-
applies: Schema2.optionalKey(Schema2.Literals(APPLIES).annotate({
|
|
101
|
-
description: `Which production and test files the budget holds: ratchet, the ones a range adds or changes, to no more overrun per rule than at the range's base; all, every one. ${SIZE_DEFAULTS.applies} when absent. The rest are listed as advisory`,
|
|
102
|
-
message: "Expected ratchet or all, and ratchet replaces changed"
|
|
103
|
-
})),
|
|
104
|
-
production: Schema2.optionalKey(ProductionBudget.annotate({
|
|
105
|
-
description: `The budget of the files under sources.production, and of the files listed as advisory outside ${TESTS_DIRECTORY}/`
|
|
106
|
-
})),
|
|
107
|
-
tests: Schema2.optionalKey(TestBudget.annotate({ description: `The budget of the files under ${TESTS_DIRECTORY}/, which sets no limit on a function's lines` }))
|
|
108
|
-
}).annotate({
|
|
109
|
-
description: "The size budget oxlint holds production and test files to, read by checks-size-budget",
|
|
110
|
-
messageUnexpectedKey: "Expected only applies, production and tests, since each limit is set inside production or tests"
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
// scripts/quality-file.ts
|
|
114
|
-
var SEGMENT = String.raw`(?!\.\.?(?:/|$))(?:\*\*|(?:[\w.@+-]|\*(?!\*))+)`;
|
|
115
|
-
var FILE = String.raw`(?:[\w.@+-]|\*(?!\*))*\.\w+`;
|
|
116
|
-
var PathGlob = Schema3.String.check(Schema3.isPattern(new RegExp(`^${SEGMENT}(?:/${SEGMENT})*/${FILE}$`), {
|
|
117
|
-
expected: "a glob from the repository root such as src/**/*.ts: a directory first, * within a segment, ** as a whole one, a file name with an extension last"
|
|
118
|
-
})).annotate({
|
|
119
|
-
identifier: "PathGlob",
|
|
120
|
-
description: "A glob from the repository root that oxlint, the Effect language service and git read alike: a directory first, * within a segment, ** as a whole one, a file name with an extension last, and no braces, ?, [ or leading ./"
|
|
121
|
-
});
|
|
122
|
-
var DocGlob = Schema3.String.check(Schema3.isPattern(new RegExp(`^(?:${SEGMENT}/)*${FILE}$`), {
|
|
123
|
-
expected: "a glob from the repository root such as README.md or docs/**/*.md: * within a segment, ** as a whole one, a file name with an extension last"
|
|
124
|
-
})).annotate({
|
|
125
|
-
identifier: "DocGlob",
|
|
126
|
-
description: "A glob from the repository root that checks-docs reads: * within a segment, ** as a whole one, a file name with an extension last"
|
|
127
|
-
});
|
|
128
|
-
var LITERAL_SEGMENT = String.raw`(?!\.\.?(?:/|$))[\w.@+-]+`;
|
|
129
|
-
var DirectoryPath = Schema3.String.check(Schema3.isPattern(new RegExp(`^${LITERAL_SEGMENT}(?:/${LITERAL_SEGMENT})*$`), {
|
|
130
|
-
expected: "a directory from the repository root such as src/billing, with no glob and no trailing slash"
|
|
131
|
-
})).annotate({ identifier: "DirectoryPath" });
|
|
132
|
-
var FilePath = Schema3.String.check(Schema3.isPattern(new RegExp(`^(?:${LITERAL_SEGMENT}/)*[\\w.@+-]*\\.\\w+$`), {
|
|
133
|
-
expected: "a file from the repository root such as src/billing/index.ts, with no glob"
|
|
134
|
-
})).annotate({ identifier: "FilePath" });
|
|
135
|
-
var PROOF_DIRECTORY = "tests/e2e/";
|
|
136
|
-
var ProofPath = Schema3.String.check(Schema3.isPattern(new RegExp(`^${PROOF_DIRECTORY}(?:${LITERAL_SEGMENT}/)*[\\w.@+-]+\\.test\\.tsx?$`), {
|
|
137
|
-
expected: `a test file under ${PROOF_DIRECTORY} such as ${PROOF_DIRECTORY}billing.test.ts`
|
|
138
|
-
})).annotate({ identifier: "ProofPath" });
|
|
139
|
-
var Command = Schema3.NonEmptyString.annotate({ identifier: "Command" });
|
|
140
|
-
var RuleName = Schema3.String.check(Schema3.isPattern(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, { expected: "a Rule name in kebab case" })).annotate({ identifier: "RuleName" });
|
|
141
|
-
var Identity = Schema3.Struct({ name: Schema3.NonEmptyString, email: Schema3.NonEmptyString }).annotate({
|
|
142
|
-
identifier: "Identity"
|
|
143
|
-
});
|
|
144
|
-
var CommitIdentity = Schema3.Struct({
|
|
145
|
-
authors: Schema3.NonEmptyArray(Identity).annotate({
|
|
146
|
-
description: "The identities allowed to author and commit, in place of the kit's default owner"
|
|
147
|
-
})
|
|
148
|
-
});
|
|
149
|
-
var Gates = Schema3.Struct({
|
|
150
|
-
ci: Schema3.optionalKey(Schema3.NonEmptyArray(Command).annotate({
|
|
151
|
-
description: "The commands CI runs on every pull request to the default branch, each one plain command"
|
|
152
|
-
})),
|
|
153
|
-
scheduled: Schema3.optionalKey(Schema3.Array(Command).annotate({ description: "The commands a cron-scheduled workflow runs" })),
|
|
154
|
-
lint: Schema3.optionalKey(LintGates)
|
|
155
|
-
});
|
|
156
|
-
var RunsOn = Schema3.NonEmptyArray(Schema3.NonEmptyString).annotate({
|
|
157
|
-
identifier: "RunsOn",
|
|
158
|
-
description: "The runner labels every job the kit generates runs on; ubuntu-latest when absent"
|
|
159
|
-
});
|
|
160
|
-
var EffectSources = Schema3.Struct({
|
|
161
|
-
paths: Schema3.NonEmptyArray(PathGlob).annotate({
|
|
162
|
-
description: "Where source is written in Effect, held to the Effect rules of oxlint and the language service"
|
|
163
|
-
}),
|
|
164
|
-
exempt: Schema3.optionalKey(Schema3.Array(PathGlob).annotate({ description: "Files under paths the Effect rules pass over" }))
|
|
165
|
-
});
|
|
166
|
-
var Library = Schema3.Struct({
|
|
167
|
-
name: Schema3.String.check(Schema3.isPattern(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, { expected: "a library name in kebab case" })).annotate({ description: "The link checks-vendor manages under repos/" }),
|
|
168
|
-
package: Schema3.NonEmptyString.annotate({ description: "The npm package whose installed version picks the tag" }),
|
|
169
|
-
repository: Schema3.NonEmptyString.annotate({ description: "The git remote checks-vendor clones the tag from" }),
|
|
170
|
-
tag: Schema3.String.check(Schema3.isPattern(/\{version\}/, { expected: "a tag template holding {version}" })).annotate({
|
|
171
|
-
description: "The tag template, with {version} for the installed version"
|
|
172
|
-
}),
|
|
173
|
-
path: Schema3.optionalKey(FilePath.annotate({ description: "The manifest inside the clone holding the version; package.json when absent" }))
|
|
174
|
-
}).annotate({ identifier: "Library" });
|
|
175
|
-
var Sources = Schema3.Struct({
|
|
176
|
-
production: Schema3.optionalKey(Schema3.Array(PathGlob).annotate({ description: "The source the repository ships, as against tests and tooling" })),
|
|
177
|
-
libraries: Schema3.optionalKey(Schema3.Array(Library).check(Schema3.makeFilter((libraries) => {
|
|
178
|
-
const repeated = duplicates(libraries.map((library) => library.name));
|
|
179
|
-
return repeated === undefined || `names ${repeated} more than once`;
|
|
180
|
-
})).annotate({ description: "The libraries checks-vendor pins to a shared read-only clone" })),
|
|
181
|
-
effect: Schema3.optionalKey(EffectSources)
|
|
182
|
-
});
|
|
183
|
-
var Feature = Schema3.Struct({
|
|
184
|
-
name: Schema3.String.check(Schema3.isPattern(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, { expected: "a feature name in kebab case" })).annotate({ description: "The owner the dependency rule and the change signal name" }),
|
|
185
|
-
root: DirectoryPath.annotate({ description: "The directory the feature owns" }),
|
|
186
|
-
entries: Schema3.NonEmptyArray(FilePath).annotate({
|
|
187
|
-
description: "The files under root that code outside it imports the feature through"
|
|
188
|
-
}),
|
|
189
|
-
allowFrom: Schema3.optionalKey(Schema3.Array(PathGlob).annotate({
|
|
190
|
-
description: "Files outside root that may import past its entries, such as a CLI or a harness; tests/ always may"
|
|
191
|
-
})),
|
|
192
|
-
proof: ProofPath.annotate({ description: "The end-to-end test that imports one of entries" })
|
|
193
|
-
}).check(Schema3.makeFilter(({ root, entries }) => {
|
|
194
|
-
const outside = entries.filter((entry) => !entry.startsWith(`${root}/`));
|
|
195
|
-
return outside.length === 0 || `lists ${outside.join(", ")} among its entries, outside its root ${root}`;
|
|
196
|
-
}));
|
|
197
|
-
function nests(outer, inner) {
|
|
198
|
-
return outer === inner || inner.startsWith(`${outer}/`);
|
|
199
|
-
}
|
|
200
|
-
function duplicates(names) {
|
|
201
|
-
const repeated = names.filter((name, index) => names.indexOf(name) !== index);
|
|
202
|
-
return repeated.length === 0 ? undefined : [...new Set(repeated)].join(", ");
|
|
203
|
-
}
|
|
204
|
-
var Features = Schema3.Array(Feature).check(Schema3.makeFilter((features) => {
|
|
205
|
-
const repeated = duplicates(features.map((feature) => feature.name));
|
|
206
|
-
if (repeated !== undefined)
|
|
207
|
-
return `names ${repeated} more than once`;
|
|
208
|
-
for (const outer of features) {
|
|
209
|
-
const inner = features.find((other) => other !== outer && nests(outer.root, other.root));
|
|
210
|
-
if (inner !== undefined)
|
|
211
|
-
return `gives ${inner.root} to both ${outer.name} and ${inner.name}`;
|
|
212
|
-
}
|
|
213
|
-
return true;
|
|
214
|
-
}));
|
|
215
|
-
var AgentRules = Schema3.Struct({
|
|
216
|
-
on: Schema3.optionalKey(Schema3.Array(RuleName).annotate({ description: "Catalogued Rules switched on here" })),
|
|
217
|
-
off: Schema3.optionalKey(Schema3.Array(RuleName).annotate({ description: "Catalogued Rules switched off here" }))
|
|
218
|
-
}).check(Schema3.makeFilter(({ on = [], off = [] }) => {
|
|
219
|
-
const both = on.filter((rule) => off.includes(rule));
|
|
220
|
-
return both.length === 0 || `switches ${both.join(", ")} both on and off`;
|
|
221
|
-
}));
|
|
222
|
-
var pagesIn = (mode) => Schema3.optionalKey(Schema3.Array(PathGlob).annotate({ description: `The pages written as ${mode}` }));
|
|
223
|
-
var Docs = Schema3.Struct({
|
|
224
|
-
pages: Schema3.optionalKey(Schema3.Struct({
|
|
225
|
-
tutorial: pagesIn("a tutorial, which teaches by building one thing"),
|
|
226
|
-
"how-to": pagesIn("a how-to, which walks one task"),
|
|
227
|
-
reference: pagesIn("reference, which describes a thing to be looked up"),
|
|
228
|
-
explanation: pagesIn("an explanation, which says why")
|
|
229
|
-
}).annotate({
|
|
230
|
-
description: "The Diátaxis mode of each page, whose template checks-docs holds the page to; a page under docs/ needs one"
|
|
231
|
-
})),
|
|
232
|
-
forConsumers: Schema3.optionalKey(Schema3.Array(DocGlob).annotate({
|
|
233
|
-
description: "The living docs that speak to a repository installing this one, whose bun run commands checks-docs does not hold to this package.json"
|
|
234
|
-
}))
|
|
235
|
-
});
|
|
236
|
-
var Quality = Schema3.Struct({
|
|
237
|
-
$schema: Schema3.optionalKey(Schema3.String),
|
|
238
|
-
defaultBranch: Schema3.optionalKey(Schema3.NonEmptyString.annotate({ description: "The branch pull requests merge into; main when absent" })),
|
|
239
|
-
gates: Schema3.optionalKey(Gates),
|
|
240
|
-
runsOn: Schema3.optionalKey(RunsOn),
|
|
241
|
-
commitIdentity: Schema3.optionalKey(CommitIdentity),
|
|
242
|
-
sources: Schema3.optionalKey(Sources),
|
|
243
|
-
size: Schema3.optionalKey(Size),
|
|
244
|
-
features: Schema3.optionalKey(Features.annotate({
|
|
245
|
-
description: "The feature owners dependency-cruiser holds to their entries and checks-feature-owners maps a change to"
|
|
246
|
-
})),
|
|
247
|
-
changeSignal: Schema3.optionalKey(Schema3.Literal("advisory").annotate({
|
|
248
|
-
description: "Report which feature owners a change touches, without failing on it"
|
|
249
|
-
})),
|
|
250
|
-
agentRules: Schema3.optionalKey(AgentRules),
|
|
251
|
-
docs: Schema3.optionalKey(Docs.annotate({ description: "What checks-docs reads to map a doc file to its template" }))
|
|
252
|
-
}).annotate({
|
|
253
|
-
title: QUALITY_FILE,
|
|
254
|
-
description: "What a repository has opted into from @avi2dg/checks, read by its bins and agent Rule selection"
|
|
255
|
-
}).check(Schema3.makeFilter(({ size, sources }) => size === undefined || (sources?.production ?? []).length > 0 || "declares size, which holds no production file without sources.production", {
|
|
256
|
-
toJsonSchema: () => ({
|
|
257
|
-
if: { required: ["size"] },
|
|
258
|
-
then: { required: ["sources"], properties: { sources: { required: ["production"], properties: { production: { minItems: 1 } } } } }
|
|
259
|
-
})
|
|
260
|
-
}), Schema3.makeFilter(({ changeSignal, features = [] }) => changeSignal === undefined || features.length > 0 || "declares changeSignal, which maps a change to no owner without features", {
|
|
261
|
-
toJsonSchema: () => ({
|
|
262
|
-
if: { required: ["changeSignal"] },
|
|
263
|
-
then: { required: ["features"], properties: { features: { minItems: 1 } } }
|
|
264
|
-
})
|
|
265
|
-
}));
|
|
266
|
-
var LegacyManifest = Schema3.Struct({
|
|
267
|
-
ciWiring: Schema3.optionalKey(Schema3.Struct({
|
|
268
|
-
gates: Schema3.optionalKey(Schema3.NonEmptyArray(Command)),
|
|
269
|
-
scheduled: Schema3.optionalKey(Schema3.Array(Command)),
|
|
270
|
-
lintGates: Schema3.optionalKey(LintGates),
|
|
271
|
-
defaultBranch: Schema3.optionalKey(Schema3.NonEmptyString)
|
|
272
|
-
})),
|
|
273
|
-
commitIdentity: Schema3.optionalKey(CommitIdentity)
|
|
274
|
-
});
|
|
275
|
-
var LEGACY_KEYS = ["ciWiring", "commitIdentity"];
|
|
276
|
-
|
|
277
|
-
class QualityUnreadable extends Schema3.TaggedError()("QualityUnreadable", {
|
|
278
|
-
message: Schema3.String
|
|
279
|
-
}) {
|
|
280
|
-
}
|
|
281
|
-
var MANIFEST = "package.json";
|
|
282
|
-
var decodeQualityJson = Schema3.decodeUnknownEffect(Schema3.fromJsonString(Quality), { onExcessProperty: "error" });
|
|
283
|
-
var decodeManifestJson = Schema3.decodeUnknownEffect(Schema3.fromJsonString(LegacyManifest));
|
|
284
|
-
var decodeQuality = (text, source) => decodeQualityJson(text).pipe(Effect.mapError((cause) => new QualityUnreadable({ message: `${source}: ${cause.message}` })));
|
|
285
|
-
function fromLegacy({ ciWiring, commitIdentity }) {
|
|
286
|
-
const gates = {
|
|
287
|
-
...ciWiring?.gates === undefined ? {} : { ci: ciWiring.gates },
|
|
288
|
-
...ciWiring?.scheduled === undefined ? {} : { scheduled: ciWiring.scheduled },
|
|
289
|
-
...ciWiring?.lintGates === undefined ? {} : { lint: ciWiring.lintGates }
|
|
290
|
-
};
|
|
291
|
-
return {
|
|
292
|
-
...ciWiring?.defaultBranch === undefined ? {} : { defaultBranch: ciWiring.defaultBranch },
|
|
293
|
-
...Object.keys(gates).length === 0 ? {} : { gates },
|
|
294
|
-
...commitIdentity === undefined ? {} : { commitIdentity }
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
var decodeManifest = (text, source) => decodeManifestJson(text).pipe(Effect.map((manifest) => ({
|
|
298
|
-
keys: LEGACY_KEYS.filter((key) => manifest[key] !== undefined),
|
|
299
|
-
quality: fromLegacy(manifest)
|
|
300
|
-
})), Effect.mapError((cause) => new QualityUnreadable({ message: `${source}: ${cause.message}` })));
|
|
301
|
-
var UNDECLARED = { keys: [], quality: {} };
|
|
302
|
-
var readQuality = Effect.fn("readQuality")(function* (root) {
|
|
303
|
-
const fs = yield* FileSystem.FileSystem;
|
|
304
|
-
const path = yield* Path.Path;
|
|
305
|
-
const read = (file) => fs.readFileString(path.join(root, file)).pipe(Effect.mapError((cause) => new QualityUnreadable({ message: `cannot read ${file}: ${cause.message}` })));
|
|
306
|
-
const legacy = (yield* fs.exists(path.join(root, MANIFEST))) ? yield* decodeManifest(yield* read(MANIFEST), MANIFEST) : UNDECLARED;
|
|
307
|
-
const keys = legacy.keys.join(" and ");
|
|
308
|
-
if (yield* fs.exists(path.join(root, QUALITY_FILE))) {
|
|
309
|
-
if (legacy.keys.length > 0) {
|
|
310
|
-
return yield* new QualityUnreadable({
|
|
311
|
-
message: `${MANIFEST} still sets ${keys}, which ${QUALITY_FILE} replaces; move what it holds there`
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
return { source: QUALITY_FILE, quality: yield* decodeQuality(yield* read(QUALITY_FILE), QUALITY_FILE) };
|
|
315
|
-
}
|
|
316
|
-
if (legacy.keys.length > 0) {
|
|
317
|
-
const them = legacy.keys.length === 1 ? "it" : "them";
|
|
318
|
-
yield* Console.error(`${MANIFEST} sets ${keys}, which a later minor release stops reading; move ${them} into ${QUALITY_FILE}`);
|
|
319
|
-
}
|
|
320
|
-
return { source: MANIFEST, quality: legacy.quality };
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
// scripts/feature-rules.ts
|
|
324
|
-
var TESTS = "^tests/";
|
|
325
|
-
function escaped(literal) {
|
|
326
|
-
return literal.replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
|
|
327
|
-
}
|
|
328
|
-
function globPattern(glob) {
|
|
329
|
-
const segments = glob.split("/");
|
|
330
|
-
const body = segments.map((segment, index) => {
|
|
331
|
-
if (segment === "**")
|
|
332
|
-
return "(?:[^/]+/)*";
|
|
333
|
-
const pattern = segment.split("*").map(escaped).join("[^/]*");
|
|
334
|
-
return index === segments.length - 1 ? pattern : `${pattern}/`;
|
|
335
|
-
});
|
|
336
|
-
return `^${body.join("")}$`;
|
|
337
|
-
}
|
|
338
|
-
function rulesFor(features) {
|
|
339
|
-
return features.map(({ name, root, entries, allowFrom = [] }) => ({
|
|
340
|
-
name: `feature-${name}-entries`,
|
|
341
|
-
severity: "error",
|
|
342
|
-
comment: `Outside ${root}/, ${name} is imported through ${entries.join(", ")}. Import one of those, or list the importer in the feature's allowFrom in quality.json.`,
|
|
343
|
-
from: { pathNot: [`^${escaped(root)}/`, TESTS, ...allowFrom.map(globPattern)] },
|
|
344
|
-
to: { path: `^${escaped(root)}/`, pathNot: entries.map((entry) => `^${escaped(entry)}$`) }
|
|
345
|
-
}));
|
|
346
|
-
}
|
|
347
|
-
var decode = Schema4.decodeUnknownSync(Quality);
|
|
348
|
-
function featureRules(quality) {
|
|
349
|
-
return rulesFor(decode(quality, { onExcessProperty: "error" }).features ?? []);
|
|
350
|
-
}
|
|
351
|
-
export {
|
|
352
|
-
globPattern,
|
|
353
|
-
featureRules
|
|
354
|
-
};
|