@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
@@ -1,353 +0,0 @@
1
- import { Console, Effect, FileSystem, JsonSchema, Path, Schema } from "effect";
2
- import { LintGates, QUALITY_FILE } from "./gates.ts";
3
- import { Size } from "./size-rules.ts";
4
-
5
- const SEGMENT = String.raw`(?!\.\.?(?:/|$))(?:\*\*|(?:[\w.@+-]|\*(?!\*))+)`;
6
-
7
- const FILE = String.raw`(?:[\w.@+-]|\*(?!\*))*\.\w+`;
8
-
9
- // oxlint matches a glob without a slash against a file's name at any depth, where tsc, the
10
- // language service and git match it at the root alone, so a glob names a directory first.
11
- // The language service drops an include ending in ** and oxlint reads a bare name as a file,
12
- // so a glob ends in a file name with an extension.
13
- const PathGlob = Schema.String.check(
14
- Schema.isPattern(new RegExp(`^${SEGMENT}(?:/${SEGMENT})*/${FILE}$`), {
15
- expected:
16
- "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",
17
- }),
18
- ).annotate({
19
- identifier: "PathGlob",
20
- description:
21
- "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 ./",
22
- });
23
-
24
- // Only checks-docs reads a doc glob, matching it from the root, so a file at the root names itself.
25
- const DocGlob = Schema.String.check(
26
- Schema.isPattern(new RegExp(`^(?:${SEGMENT}/)*${FILE}$`), {
27
- 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",
28
- }),
29
- ).annotate({
30
- identifier: "DocGlob",
31
- 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",
32
- });
33
-
34
- const LITERAL_SEGMENT = String.raw`(?!\.\.?(?:/|$))[\w.@+-]+`;
35
-
36
- const DirectoryPath = Schema.String.check(
37
- Schema.isPattern(new RegExp(`^${LITERAL_SEGMENT}(?:/${LITERAL_SEGMENT})*$`), {
38
- expected: "a directory from the repository root such as src/billing, with no glob and no trailing slash",
39
- }),
40
- ).annotate({ identifier: "DirectoryPath" });
41
-
42
- const FilePath = Schema.String.check(
43
- Schema.isPattern(new RegExp(`^(?:${LITERAL_SEGMENT}/)*[\\w.@+-]*\\.\\w+$`), {
44
- expected: "a file from the repository root such as src/billing/index.ts, with no glob",
45
- }),
46
- ).annotate({ identifier: "FilePath" });
47
-
48
- export const PROOF_DIRECTORY = "tests/e2e/";
49
-
50
- const ProofPath = Schema.String.check(
51
- Schema.isPattern(new RegExp(`^${PROOF_DIRECTORY}(?:${LITERAL_SEGMENT}/)*[\\w.@+-]+\\.test\\.tsx?$`), {
52
- expected: `a test file under ${PROOF_DIRECTORY} such as ${PROOF_DIRECTORY}billing.test.ts`,
53
- }),
54
- ).annotate({ identifier: "ProofPath" });
55
-
56
- const Command = Schema.NonEmptyString.annotate({ identifier: "Command" });
57
-
58
- const RuleName = Schema.String.check(
59
- Schema.isPattern(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, { expected: "a Rule name in kebab case" }),
60
- ).annotate({ identifier: "RuleName" });
61
-
62
- export const Identity = Schema.Struct({ name: Schema.NonEmptyString, email: Schema.NonEmptyString }).annotate({
63
- identifier: "Identity",
64
- });
65
- export type Identity = typeof Identity.Type;
66
-
67
- const CommitIdentity = Schema.Struct({
68
- authors: Schema.NonEmptyArray(Identity).annotate({
69
- description: "The identities allowed to author and commit, in place of the kit's default owner",
70
- }),
71
- });
72
-
73
- const Gates = Schema.Struct({
74
- ci: Schema.optionalKey(
75
- Schema.NonEmptyArray(Command).annotate({
76
- description: "The commands CI runs on every pull request to the default branch, each one plain command",
77
- }),
78
- ),
79
- scheduled: Schema.optionalKey(
80
- Schema.Array(Command).annotate({ description: "The commands a cron-scheduled workflow runs" }),
81
- ),
82
- lint: Schema.optionalKey(LintGates),
83
- });
84
-
85
- const RunsOn = Schema.NonEmptyArray(Schema.NonEmptyString).annotate({
86
- identifier: "RunsOn",
87
- description: "The runner labels every job the kit generates runs on; ubuntu-latest when absent",
88
- });
89
-
90
- const EffectSources = Schema.Struct({
91
- paths: Schema.NonEmptyArray(PathGlob).annotate({
92
- description: "Where source is written in Effect, held to the Effect rules of oxlint and the language service",
93
- }),
94
- exempt: Schema.optionalKey(
95
- Schema.Array(PathGlob).annotate({ description: "Files under paths the Effect rules pass over" }),
96
- ),
97
- });
98
-
99
- const Library = Schema.Struct({
100
- name: Schema.String.check(
101
- Schema.isPattern(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, { expected: "a library name in kebab case" }),
102
- ).annotate({ description: "The link checks-vendor manages under repos/" }),
103
- package: Schema.NonEmptyString.annotate({ description: "The npm package whose installed version picks the tag" }),
104
- repository: Schema.NonEmptyString.annotate({ description: "The git remote checks-vendor clones the tag from" }),
105
- tag: Schema.String.check(Schema.isPattern(/\{version\}/, { expected: "a tag template holding {version}" })).annotate({
106
- description: "The tag template, with {version} for the installed version",
107
- }),
108
- path: Schema.optionalKey(
109
- FilePath.annotate({ description: "The manifest inside the clone holding the version; package.json when absent" }),
110
- ),
111
- }).annotate({ identifier: "Library" });
112
- export type Library = typeof Library.Type;
113
-
114
- const Sources = Schema.Struct({
115
- production: Schema.optionalKey(
116
- Schema.Array(PathGlob).annotate({ description: "The source the repository ships, as against tests and tooling" }),
117
- ),
118
- libraries: Schema.optionalKey(
119
- Schema.Array(Library).check(
120
- Schema.makeFilter((libraries) => {
121
- const repeated = duplicates(libraries.map((library) => library.name));
122
- return repeated === undefined || `names ${repeated} more than once`;
123
- }),
124
- ).annotate({ description: "The libraries checks-vendor pins to a shared read-only clone" }),
125
- ),
126
- effect: Schema.optionalKey(EffectSources),
127
- });
128
-
129
- const Feature = Schema.Struct({
130
- name: Schema.String.check(
131
- Schema.isPattern(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, { expected: "a feature name in kebab case" }),
132
- ).annotate({ description: "The owner the dependency rule and the change signal name" }),
133
- root: DirectoryPath.annotate({ description: "The directory the feature owns" }),
134
- entries: Schema.NonEmptyArray(FilePath).annotate({
135
- description: "The files under root that code outside it imports the feature through",
136
- }),
137
- allowFrom: Schema.optionalKey(
138
- Schema.Array(PathGlob).annotate({
139
- description: "Files outside root that may import past its entries, such as a CLI or a harness; tests/ always may",
140
- }),
141
- ),
142
- proof: ProofPath.annotate({ description: "The end-to-end test that imports one of entries" }),
143
- }).check(
144
- Schema.makeFilter(({ root, entries }) => {
145
- const outside = entries.filter((entry) => !entry.startsWith(`${root}/`));
146
- return outside.length === 0 || `lists ${outside.join(", ")} among its entries, outside its root ${root}`;
147
- }),
148
- );
149
- export type Feature = typeof Feature.Type;
150
-
151
- function nests(outer: string, inner: string): boolean {
152
- return outer === inner || inner.startsWith(`${outer}/`);
153
- }
154
-
155
- function duplicates(names: readonly string[]): string | undefined {
156
- const repeated = names.filter((name, index) => names.indexOf(name) !== index);
157
- return repeated.length === 0 ? undefined : [...new Set(repeated)].join(", ");
158
- }
159
-
160
- const Features = Schema.Array(Feature).check(
161
- Schema.makeFilter((features) => {
162
- const repeated = duplicates(features.map((feature) => feature.name));
163
- if (repeated !== undefined) return `names ${repeated} more than once`;
164
- for (const outer of features) {
165
- const inner = features.find((other) => other !== outer && nests(outer.root, other.root));
166
- if (inner !== undefined) return `gives ${inner.root} to both ${outer.name} and ${inner.name}`;
167
- }
168
- return true;
169
- }),
170
- );
171
-
172
- const AgentRules = Schema.Struct({
173
- on: Schema.optionalKey(Schema.Array(RuleName).annotate({ description: "Catalogued Rules switched on here" })),
174
- off: Schema.optionalKey(Schema.Array(RuleName).annotate({ description: "Catalogued Rules switched off here" })),
175
- }).check(
176
- Schema.makeFilter(({ on = [], off = [] }) => {
177
- const both = on.filter((rule) => off.includes(rule));
178
- return both.length === 0 || `switches ${both.join(", ")} both on and off`;
179
- }),
180
- );
181
-
182
- export const MODES = ["tutorial", "how-to", "reference", "explanation"] as const;
183
- export type Mode = (typeof MODES)[number];
184
-
185
- const pagesIn = (mode: string) =>
186
- Schema.optionalKey(Schema.Array(PathGlob).annotate({ description: `The pages written as ${mode}` }));
187
-
188
- const Docs = Schema.Struct({
189
- pages: Schema.optionalKey(
190
- Schema.Struct({
191
- tutorial: pagesIn("a tutorial, which teaches by building one thing"),
192
- "how-to": pagesIn("a how-to, which walks one task"),
193
- reference: pagesIn("reference, which describes a thing to be looked up"),
194
- explanation: pagesIn("an explanation, which says why"),
195
- } satisfies Record<Mode, unknown>).annotate({
196
- description: "The Diátaxis mode of each page, whose template checks-docs holds the page to; a page under docs/ needs one",
197
- }),
198
- ),
199
- forConsumers: Schema.optionalKey(
200
- Schema.Array(DocGlob).annotate({
201
- 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",
202
- }),
203
- ),
204
- });
205
- export type Docs = typeof Docs.Type;
206
-
207
- export const Quality = Schema.Struct({
208
- $schema: Schema.optionalKey(Schema.String),
209
- defaultBranch: Schema.optionalKey(
210
- Schema.NonEmptyString.annotate({ description: "The branch pull requests merge into; main when absent" }),
211
- ),
212
- gates: Schema.optionalKey(Gates),
213
- runsOn: Schema.optionalKey(RunsOn),
214
- commitIdentity: Schema.optionalKey(CommitIdentity),
215
- sources: Schema.optionalKey(Sources),
216
- size: Schema.optionalKey(Size),
217
- features: Schema.optionalKey(
218
- Features.annotate({
219
- description: "The feature owners dependency-cruiser holds to their entries and checks-feature-owners maps a change to",
220
- }),
221
- ),
222
- changeSignal: Schema.optionalKey(
223
- Schema.Literal("advisory").annotate({
224
- description: "Report which feature owners a change touches, without failing on it",
225
- }),
226
- ),
227
- agentRules: Schema.optionalKey(AgentRules),
228
- docs: Schema.optionalKey(Docs.annotate({ description: "What checks-docs reads to map a doc file to its template" })),
229
- })
230
- .annotate({
231
- title: QUALITY_FILE,
232
- description: "What a repository has opted into from @avi2dg/checks, read by its bins and agent Rule selection",
233
- })
234
- .check(
235
- Schema.makeFilter(
236
- ({ size, sources }) =>
237
- size === undefined || (sources?.production ?? []).length > 0 || "declares size, which holds no production file without sources.production",
238
- {
239
- toJsonSchema: () => ({
240
- if: { required: ["size"] },
241
- then: { required: ["sources"], properties: { sources: { required: ["production"], properties: { production: { minItems: 1 } } } } },
242
- }),
243
- },
244
- ),
245
- Schema.makeFilter(
246
- ({ changeSignal, features = [] }) =>
247
- changeSignal === undefined || features.length > 0 || "declares changeSignal, which maps a change to no owner without features",
248
- {
249
- toJsonSchema: () => ({
250
- if: { required: ["changeSignal"] },
251
- then: { required: ["features"], properties: { features: { minItems: 1 } } },
252
- }),
253
- },
254
- ),
255
- );
256
- export type Quality = typeof Quality.Type;
257
-
258
- export const LegacyManifest = Schema.Struct({
259
- ciWiring: Schema.optionalKey(
260
- Schema.Struct({
261
- gates: Schema.optionalKey(Schema.NonEmptyArray(Command)),
262
- scheduled: Schema.optionalKey(Schema.Array(Command)),
263
- lintGates: Schema.optionalKey(LintGates),
264
- defaultBranch: Schema.optionalKey(Schema.NonEmptyString),
265
- }),
266
- ),
267
- commitIdentity: Schema.optionalKey(CommitIdentity),
268
- });
269
- type LegacyManifest = typeof LegacyManifest.Type;
270
-
271
- const LEGACY_KEYS = ["ciWiring", "commitIdentity"] as const;
272
-
273
- export type LegacyDeclaration = {
274
- readonly keys: readonly (typeof LEGACY_KEYS)[number][];
275
- readonly quality: Quality;
276
- };
277
-
278
- export class QualityUnreadable extends Schema.TaggedError<QualityUnreadable>()("QualityUnreadable", {
279
- message: Schema.String,
280
- }) {}
281
-
282
- const MANIFEST = "package.json";
283
-
284
- export type Declared = {
285
- readonly source: typeof QUALITY_FILE | typeof MANIFEST;
286
- readonly quality: Quality;
287
- };
288
-
289
- const decodeQualityJson = Schema.decodeUnknownEffect(Schema.fromJsonString(Quality), { onExcessProperty: "error" });
290
- const decodeManifestJson = Schema.decodeUnknownEffect(Schema.fromJsonString(LegacyManifest));
291
-
292
- export const decodeQuality = (text: string, source: string): Effect.Effect<Quality, QualityUnreadable> =>
293
- decodeQualityJson(text).pipe(Effect.mapError((cause) => new QualityUnreadable({ message: `${source}: ${cause.message}` })));
294
-
295
- function fromLegacy({ ciWiring, commitIdentity }: LegacyManifest): Quality {
296
- const gates = {
297
- ...(ciWiring?.gates === undefined ? {} : { ci: ciWiring.gates }),
298
- ...(ciWiring?.scheduled === undefined ? {} : { scheduled: ciWiring.scheduled }),
299
- ...(ciWiring?.lintGates === undefined ? {} : { lint: ciWiring.lintGates }),
300
- };
301
- return {
302
- ...(ciWiring?.defaultBranch === undefined ? {} : { defaultBranch: ciWiring.defaultBranch }),
303
- ...(Object.keys(gates).length === 0 ? {} : { gates }),
304
- ...(commitIdentity === undefined ? {} : { commitIdentity }),
305
- };
306
- }
307
-
308
- export const decodeManifest = (text: string, source: string): Effect.Effect<LegacyDeclaration, QualityUnreadable> =>
309
- decodeManifestJson(text).pipe(
310
- Effect.map((manifest) => ({
311
- keys: LEGACY_KEYS.filter((key) => manifest[key] !== undefined),
312
- quality: fromLegacy(manifest),
313
- })),
314
- Effect.mapError((cause) => new QualityUnreadable({ message: `${source}: ${cause.message}` })),
315
- );
316
-
317
- const UNDECLARED: LegacyDeclaration = { keys: [], quality: {} };
318
-
319
- export const readQuality = Effect.fn("readQuality")(function* (root: string) {
320
- const fs = yield* FileSystem.FileSystem;
321
- const path = yield* Path.Path;
322
- const read = (file: string) =>
323
- fs
324
- .readFileString(path.join(root, file))
325
- .pipe(Effect.mapError((cause) => new QualityUnreadable({ message: `cannot read ${file}: ${cause.message}` })));
326
-
327
- const legacy = (yield* fs.exists(path.join(root, MANIFEST)))
328
- ? yield* decodeManifest(yield* read(MANIFEST), MANIFEST)
329
- : UNDECLARED;
330
- const keys = legacy.keys.join(" and ");
331
- if (yield* fs.exists(path.join(root, QUALITY_FILE))) {
332
- if (legacy.keys.length > 0) {
333
- return yield* new QualityUnreadable({
334
- message: `${MANIFEST} still sets ${keys}, which ${QUALITY_FILE} replaces; move what it holds there`,
335
- });
336
- }
337
- return { source: QUALITY_FILE, quality: yield* decodeQuality(yield* read(QUALITY_FILE), QUALITY_FILE) } satisfies Declared;
338
- }
339
- if (legacy.keys.length > 0) {
340
- const them = legacy.keys.length === 1 ? "it" : "them";
341
- yield* Console.error(`${MANIFEST} sets ${keys}, which a later minor release stops reading; move ${them} into ${QUALITY_FILE}`);
342
- }
343
- return { source: MANIFEST, quality: legacy.quality } satisfies Declared;
344
- });
345
-
346
- export function renderJson(value: unknown): string {
347
- return `${JSON.stringify(value, null, 2)}\n`;
348
- }
349
-
350
- export function qualityJsonSchema(): JsonSchema.JsonSchema {
351
- const { schema, definitions } = Schema.toJsonSchemaDocument(Quality, { onExcessProperty: "error" });
352
- return { $schema: JsonSchema.META_SCHEMA_URI_DRAFT_2020_12, ...schema, $defs: definitions };
353
- }