@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.
Files changed (57) hide show
  1. package/CHANGELOG.md +66 -40
  2. package/CONTRIBUTING.md +14 -10
  3. package/README.md +15 -24
  4. package/bunfig.toml +1 -1
  5. package/dist/effect-channel/index.js +122 -0
  6. package/dist/{index.js → readability/index.js} +8 -120
  7. package/docs/configs/commit-messages.md +5 -1
  8. package/docs/configs/dependency-rules.md +5 -2
  9. package/docs/configs/effect-rules.md +32 -33
  10. package/docs/configs/native-settings.md +74 -0
  11. package/docs/configs/typescript-rules.md +4 -0
  12. package/docs/design.md +26 -27
  13. package/docs/gates/checks-backtest.md +4 -0
  14. package/docs/gates/checks-ci-wiring.md +30 -93
  15. package/docs/gates/checks-comment-gate.md +4 -0
  16. package/docs/gates/checks-commit-identity.md +23 -27
  17. package/docs/gates/checks-docs.md +23 -21
  18. package/docs/gates/checks-flake.md +4 -10
  19. package/docs/gates/checks-lint-coverage.md +5 -1
  20. package/docs/gates/checks-lint.md +22 -104
  21. package/docs/gates/checks-mutation-compare.md +4 -0
  22. package/docs/gates/checks-quarantine-clock.md +4 -0
  23. package/docs/gates/checks-repetition.md +27 -41
  24. package/docs/gates/checks-subsumed-tests.md +4 -0
  25. package/docs/gates/checks-suppressions-ratchet.md +4 -0
  26. package/docs/gates/checks-test-layout.md +16 -8
  27. package/docs/gates/checks-test.md +68 -36
  28. package/docs/gates/checks-vendor.md +20 -13
  29. package/oxlintrc.json +1 -1
  30. package/package.json +10 -21
  31. package/scripts/ci-wiring.ts +28 -97
  32. package/scripts/commit-identity.ts +32 -4
  33. package/scripts/doc-rules.ts +26 -11
  34. package/scripts/doc-templates.ts +2 -1
  35. package/scripts/docs.ts +4 -7
  36. package/scripts/gates.ts +0 -29
  37. package/scripts/git.ts +24 -1
  38. package/scripts/lint.ts +15 -34
  39. package/scripts/range-gate.ts +1 -2
  40. package/scripts/repetition.ts +51 -40
  41. package/scripts/shell-command.ts +7 -1
  42. package/scripts/swc.ts +46 -0
  43. package/scripts/test-layout.ts +40 -56
  44. package/scripts/test-skips.ts +180 -0
  45. package/scripts/test.ts +33 -38
  46. package/scripts/vendor.ts +55 -11
  47. package/dist/feature-rules.js +0 -354
  48. package/docs/configs/quality-file.md +0 -103
  49. package/docs/gates/checks-feature-owners.md +0 -113
  50. package/docs/gates/checks-quality.md +0 -111
  51. package/docs/gates/checks-size-budget.md +0 -107
  52. package/quality.schema.json +0 -514
  53. package/scripts/feature-owners.ts +0 -139
  54. package/scripts/quality-file.ts +0 -353
  55. package/scripts/quality.ts +0 -363
  56. package/scripts/size-budget.ts +0 -285
  57. 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
- const Environment = Schema.Literals(["ci", "local"]);
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 DECLARATIONS = "package.json testSkips";
35
- const USAGE = `usage: ${NAME} takes no arguments, since a narrowed run skips every test it leaves out; run bun test --randomize <args> for one`;
36
-
37
- const decodeManifest = Schema.decodeUnknownEffect(
38
- Schema.fromJsonString(Schema.Struct({ testSkips: Schema.optionalKey(Schema.Array(SkipDeclaration)) })),
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
- return declaration.file === result.file && declaration.test === result.name;
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((result) => result.outcome === "skipped" || result.outcome === "todo");
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}${NAME_SEPARATOR}${declaration.test}: declared, but no such test skipped; delete the declaration`;
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 in ${DECLARATIONS}${aside}`];
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 = result.outcome === "todo" ? "a todo" : "skipped";
91
- return ` ${result.file}:${result.line} ${result.name}: ${kind} with no declaration; run it, or declare it in ${DECLARATIONS} with its reason`;
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 args = ["test", "--randomize", ...reporterArgs(outfile)];
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
- if (process.argv.length > 2) return yield* new Usage({ message: USAGE });
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* readDeclarations(process.cwd());
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} takes no arguments, since quality.json names the libraries`;
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 [extra] = process.argv.slice(2);
300
- if (extra !== undefined) return yield* new Usage({ message: USAGE });
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) {
@@ -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
- };