@avi2dg/checks 0.20.0 → 0.22.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 (54) hide show
  1. package/CHANGELOG.md +65 -39
  2. package/CONTRIBUTING.md +11 -8
  3. package/README.md +15 -24
  4. package/bunfig.toml +1 -1
  5. package/docs/configs/commit-messages.md +5 -1
  6. package/docs/configs/dependency-rules.md +5 -2
  7. package/docs/configs/effect-rules.md +32 -33
  8. package/docs/configs/native-settings.md +74 -0
  9. package/docs/configs/typescript-rules.md +4 -0
  10. package/docs/design.md +24 -25
  11. package/docs/gates/checks-backtest.md +4 -0
  12. package/docs/gates/checks-ci-wiring.md +30 -93
  13. package/docs/gates/checks-comment-gate.md +4 -0
  14. package/docs/gates/checks-commit-identity.md +23 -27
  15. package/docs/gates/checks-docs.md +23 -21
  16. package/docs/gates/checks-flake.md +4 -10
  17. package/docs/gates/checks-lint-coverage.md +5 -1
  18. package/docs/gates/checks-lint.md +22 -104
  19. package/docs/gates/checks-mutation-compare.md +4 -0
  20. package/docs/gates/checks-quarantine-clock.md +4 -0
  21. package/docs/gates/checks-repetition.md +26 -41
  22. package/docs/gates/checks-subsumed-tests.md +4 -0
  23. package/docs/gates/checks-suppressions-ratchet.md +4 -0
  24. package/docs/gates/checks-test-layout.md +16 -8
  25. package/docs/gates/checks-test.md +68 -36
  26. package/docs/gates/checks-vendor.md +20 -13
  27. package/package.json +8 -21
  28. package/scripts/ci-wiring.ts +28 -97
  29. package/scripts/commit-identity.ts +32 -4
  30. package/scripts/doc-rules.ts +26 -11
  31. package/scripts/doc-templates.ts +2 -1
  32. package/scripts/docs.ts +4 -7
  33. package/scripts/gates.ts +0 -29
  34. package/scripts/git.ts +24 -1
  35. package/scripts/lint.ts +15 -34
  36. package/scripts/range-gate.ts +1 -2
  37. package/scripts/repetition.ts +40 -40
  38. package/scripts/shell-command.ts +7 -1
  39. package/scripts/swc.ts +46 -0
  40. package/scripts/test-layout.ts +40 -56
  41. package/scripts/test-skips.ts +180 -0
  42. package/scripts/test.ts +33 -38
  43. package/scripts/vendor.ts +55 -11
  44. package/dist/feature-rules.js +0 -349
  45. package/docs/configs/quality-file.md +0 -101
  46. package/docs/gates/checks-feature-owners.md +0 -113
  47. package/docs/gates/checks-quality.md +0 -109
  48. package/docs/gates/checks-size-budget.md +0 -107
  49. package/quality.schema.json +0 -496
  50. package/scripts/feature-owners.ts +0 -139
  51. package/scripts/quality-file.ts +0 -347
  52. package/scripts/quality.ts +0 -342
  53. package/scripts/size-budget.ts +0 -285
  54. package/scripts/size-rules.ts +0 -126
@@ -0,0 +1,180 @@
1
+ import { Effect, FileSystem, Path, Schema } from "effect";
2
+ import { identifierName, isRecord, lineOf, parseTypeScript, spanStart, stringValue } from "./swc.ts";
3
+
4
+ export type Environment = "ci" | "local";
5
+ export type TestTier = "live" | "pixel";
6
+ export type SkipDeclaration = {
7
+ readonly file: string;
8
+ readonly line: number;
9
+ readonly name: string;
10
+ readonly reason: string;
11
+ readonly when?: Environment;
12
+ };
13
+
14
+ export class SkipDeclarationError extends Schema.TaggedError<SkipDeclarationError>()("SkipDeclarationError", {
15
+ message: Schema.String,
16
+ }) {}
17
+
18
+ const TEST_FILES = ["tests/**/*.test.ts", "tests/**/*.test.tsx"] as const;
19
+ const SKIP_HELPER_MODULE = "test-skips.ts";
20
+ const whenSchema = Schema.Literals(["ci", "local"]);
21
+
22
+ export function skipReason(_reason: string, label: string, _when?: Environment): string {
23
+ return label;
24
+ }
25
+
26
+ function expressionOf(argument: unknown): unknown {
27
+ return isRecord(argument) ? argument["expression"] : undefined;
28
+ }
29
+
30
+ function argumentsOf(node: Record<string, unknown>): readonly unknown[] {
31
+ const args = node["arguments"];
32
+ return Array.isArray(args) ? args : [];
33
+ }
34
+
35
+ function skipReasonAliases(statement: unknown): readonly string[] {
36
+ if (!isRecord(statement) || statement["type"] !== "ImportDeclaration") return [];
37
+ const source = stringValue(statement["source"]);
38
+ const specifiers = statement["specifiers"];
39
+ if (source === undefined || !source.endsWith(SKIP_HELPER_MODULE) || !Array.isArray(specifiers)) return [];
40
+ return specifiers.flatMap((specifier) => {
41
+ if (!isRecord(specifier) || specifier["type"] !== "ImportSpecifier") return [];
42
+ const imported = identifierName(specifier["imported"]) ?? identifierName(specifier["local"]);
43
+ const local = identifierName(specifier["local"]);
44
+ return imported === "skipReason" && local !== undefined ? [local] : [];
45
+ });
46
+ }
47
+
48
+ function importedSkipReasonNames(module: unknown): ReadonlySet<string> {
49
+ if (!isRecord(module) || !Array.isArray(module["body"])) return new Set();
50
+ return new Set(module["body"].flatMap(skipReasonAliases));
51
+ }
52
+
53
+ type Member = { readonly object: string | undefined; readonly property: string | undefined };
54
+
55
+ function memberOf(node: unknown): Member | undefined {
56
+ if (!isRecord(node) || node["type"] !== "MemberExpression") return undefined;
57
+ return { object: identifierName(node["object"]), property: identifierName(node["property"]) };
58
+ }
59
+
60
+ type SkipReasonScan =
61
+ | { readonly kind: "none" }
62
+ | { readonly kind: "invalid"; readonly message: string }
63
+ | { readonly kind: "valid"; readonly name: string; readonly reason: string; readonly when?: Environment };
64
+
65
+ function skipReasonCall(node: unknown, aliases: ReadonlySet<string>): SkipReasonScan {
66
+ if (!isRecord(node) || node["type"] !== "CallExpression") return { kind: "none" };
67
+ if (!aliases.has(identifierName(node["callee"]) ?? "")) return { kind: "none" };
68
+ const [reasonArgument, labelArgument, whenArgument] = argumentsOf(node);
69
+ const reason = stringValue(expressionOf(reasonArgument));
70
+ const label = stringValue(expressionOf(labelArgument));
71
+ if (reason === undefined || reason.trim() === "" || label === undefined || label.trim() === "") {
72
+ return { kind: "invalid", message: "skipReason needs a non-empty literal reason and a literal test name" };
73
+ }
74
+ const when = whenArgument === undefined ? undefined : stringValue(expressionOf(whenArgument));
75
+ if (whenArgument !== undefined && when === undefined) {
76
+ return { kind: "invalid", message: "skipReason's when value must be \"ci\" or \"local\"" };
77
+ }
78
+ if (when !== undefined && !Schema.is(whenSchema)(when)) {
79
+ return {
80
+ kind: "invalid",
81
+ message: `skipReason's when value must be "ci" or "local", found ${JSON.stringify(when)}`,
82
+ };
83
+ }
84
+ return { kind: "valid", name: label, reason, ...(when === undefined ? {} : { when }) };
85
+ }
86
+
87
+ type RegisteredSkip = { readonly onDescribe: boolean; readonly first: unknown };
88
+
89
+ function registeredSkip(node: Record<string, unknown>): RegisteredSkip | undefined {
90
+ if (node["type"] !== "CallExpression") return undefined;
91
+ const callee = node["callee"];
92
+ const curried = isRecord(callee) && callee["type"] === "CallExpression";
93
+ const member = memberOf(curried ? callee["callee"] : callee);
94
+ const methods = curried ? ["skipIf", "if"] : ["skip", "todo"];
95
+ if (member === undefined || !methods.includes(member.property ?? "")) return undefined;
96
+ return { onDescribe: member.object === "describe", first: expressionOf(argumentsOf(node)[0]) };
97
+ }
98
+
99
+ function registeredSkips(module: unknown): readonly RegisteredSkip[] {
100
+ const found: RegisteredSkip[] = [];
101
+ const visit = (node: unknown): void => {
102
+ if (Array.isArray(node)) {
103
+ for (const child of node) visit(child);
104
+ return;
105
+ }
106
+ if (!isRecord(node)) return;
107
+ const registered = registeredSkip(node);
108
+ if (registered !== undefined) found.push(registered);
109
+ for (const value of Object.values(node)) visit(value);
110
+ };
111
+ visit(module);
112
+ return found;
113
+ }
114
+
115
+ type DeclarationsRead =
116
+ | { readonly kind: "valid"; readonly declarations: readonly SkipDeclaration[] }
117
+ | { readonly kind: "invalid"; readonly message: string };
118
+
119
+ function declarationsIn(source: string, file: string, module: unknown): DeclarationsRead {
120
+ const aliases = importedSkipReasonNames(module);
121
+ if (aliases.size === 0) return { kind: "valid", declarations: [] };
122
+ const declarations: SkipDeclaration[] = [];
123
+ for (const { onDescribe, first } of registeredSkips(module)) {
124
+ if (!isRecord(first)) continue;
125
+ const metadata = skipReasonCall(first, aliases);
126
+ if (metadata.kind === "none") continue;
127
+ const line = lineOf(source, spanStart(first));
128
+ if (onDescribe) {
129
+ return {
130
+ kind: "invalid",
131
+ message: `line ${line}: skipReason cannot declare a describe; declare each test inside it with its own skipReason`,
132
+ };
133
+ }
134
+ if (metadata.kind === "invalid") return metadata;
135
+ const { name, reason, when } = metadata;
136
+ declarations.push({ file, line, name, reason, ...(when === undefined ? {} : { when }) });
137
+ }
138
+ return { kind: "valid", declarations };
139
+ }
140
+
141
+ function selectedTierFile(file: string, tier: TestTier | undefined): boolean {
142
+ if (file.startsWith("tests/quarantine/")) return false;
143
+ if (tier === undefined) return !file.startsWith("tests/live/") && !file.startsWith("tests/pixel/");
144
+ return file.startsWith(`tests/${tier}/`);
145
+ }
146
+
147
+ export const readSkipDeclarations = Effect.fn("readSkipDeclarations")(function* (
148
+ root: string,
149
+ tier: TestTier | undefined,
150
+ ) {
151
+ const fs = yield* FileSystem.FileSystem;
152
+ const path = yield* Path.Path;
153
+ const files = yield* Effect.tryPromise({
154
+ try: () =>
155
+ Promise.all(
156
+ TEST_FILES.map((pattern) =>
157
+ Array.fromAsync(new Bun.Glob(pattern).scan({ cwd: root, onlyFiles: true })),
158
+ ),
159
+ ).then((groups) => groups.flat().filter((file) => selectedTierFile(file, tier)).sort()),
160
+ catch: (cause) => new SkipDeclarationError({ message: `cannot find test files: ${String(cause)}` }),
161
+ });
162
+ const declarations: SkipDeclaration[] = [];
163
+ for (const file of files) {
164
+ const source = yield* fs.readFileString(path.join(root, file)).pipe(
165
+ Effect.mapError((cause) => new SkipDeclarationError({ message: `cannot read ${file}: ${cause.message}` })),
166
+ );
167
+ const module = yield* parseTypeScript(file, source).pipe(
168
+ Effect.mapError((error) => new SkipDeclarationError({ message: error.message })),
169
+ );
170
+ const parsed = yield* Effect.try({
171
+ try: () => declarationsIn(source, file, module),
172
+ catch: (cause) => new SkipDeclarationError({ message: `cannot read skip declarations in ${file}: ${String(cause)}` }),
173
+ });
174
+ if (parsed.kind === "invalid") {
175
+ return yield* new SkipDeclarationError({ message: `${file}: ${parsed.message}` });
176
+ }
177
+ declarations.push(...parsed.declarations);
178
+ }
179
+ return declarations;
180
+ });
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) {