@avi2dg/checks 0.9.0 → 0.11.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avi2dg/checks",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Deterministic checks shared across the captain's TypeScript repos",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -21,6 +21,9 @@
21
21
  "scripts/gates.ts",
22
22
  "scripts/lint.ts",
23
23
  "scripts/test-layout.ts",
24
+ "scripts/test.ts",
25
+ "scripts/test-report.ts",
26
+ "scripts/flake.ts",
24
27
  "scripts/commit-identity.ts",
25
28
  "scripts/mutation-compare.ts",
26
29
  "scripts/ci-wiring.ts",
@@ -29,6 +32,11 @@
29
32
  "scripts/comment-gate.ts",
30
33
  "scripts/suppressions-ratchet.ts",
31
34
  "scripts/backtest.ts",
35
+ "scripts/quality-file.ts",
36
+ "scripts/quality.ts",
37
+ "presets/effect.oxlint.json",
38
+ "presets/effect.language-service.json",
39
+ "quality.schema.json",
32
40
  "oxlintrc.json",
33
41
  "stryker.preset.js",
34
42
  "tsconfig.effect.json",
@@ -41,6 +49,8 @@
41
49
  "./scripts/lint-coverage.sh": "./scripts/lint-coverage.sh",
42
50
  "./scripts/lint.ts": "./scripts/lint.ts",
43
51
  "./scripts/test-layout.ts": "./scripts/test-layout.ts",
52
+ "./scripts/test.ts": "./scripts/test.ts",
53
+ "./scripts/flake.ts": "./scripts/flake.ts",
44
54
  "./scripts/commit-identity.ts": "./scripts/commit-identity.ts",
45
55
  "./scripts/mutation-compare.ts": "./scripts/mutation-compare.ts",
46
56
  "./scripts/ci-wiring.ts": "./scripts/ci-wiring.ts",
@@ -49,6 +59,11 @@
49
59
  "./scripts/comment-gate.ts": "./scripts/comment-gate.ts",
50
60
  "./scripts/suppressions-ratchet.ts": "./scripts/suppressions-ratchet.ts",
51
61
  "./scripts/backtest.ts": "./scripts/backtest.ts",
62
+ "./scripts/quality-file.ts": "./scripts/quality-file.ts",
63
+ "./scripts/quality.ts": "./scripts/quality.ts",
64
+ "./presets/effect.oxlint.json": "./presets/effect.oxlint.json",
65
+ "./presets/effect.language-service.json": "./presets/effect.language-service.json",
66
+ "./quality.schema.json": "./quality.schema.json",
52
67
  "./oxlintrc.json": "./oxlintrc.json",
53
68
  "./stryker.preset.js": "./stryker.preset.js",
54
69
  "./tsconfig.effect.json": "./tsconfig.effect.json",
@@ -58,28 +73,21 @@
58
73
  "checks-lint": "scripts/lint.ts",
59
74
  "checks-lint-coverage": "scripts/lint-coverage.sh",
60
75
  "checks-test-layout": "scripts/test-layout.ts",
76
+ "checks-test": "scripts/test.ts",
77
+ "checks-flake": "scripts/flake.ts",
61
78
  "checks-commit-identity": "scripts/commit-identity.ts",
62
79
  "checks-mutation-compare": "scripts/mutation-compare.ts",
63
80
  "checks-ci-wiring": "scripts/ci-wiring.ts",
64
81
  "checks-comment-gate": "scripts/comment-gate.ts",
65
82
  "checks-suppressions-ratchet": "scripts/suppressions-ratchet.ts",
66
- "checks-backtest": "scripts/backtest.ts"
83
+ "checks-backtest": "scripts/backtest.ts",
84
+ "checks-quality": "scripts/quality.ts"
67
85
  },
68
86
  "scripts": {
69
- "build": "bun build effect-channel/index.ts --outdir dist --target node --format esm",
87
+ "build": "bun build effect-channel/index.ts --outdir dist --target node --format esm && bun scripts/quality-schema.ts",
70
88
  "lint": "oxlint --type-aware && bun scripts/lint.ts && depcruise --config .dependency-cruiser.cjs .",
71
89
  "typecheck": "tsc --noEmit && effect-tsgo diagnostics --project tsconfig.json --format text --strict",
72
- "test": "bun test --randomize"
73
- },
74
- "ciWiring": {
75
- "gates": [
76
- "bun run build",
77
- "git diff --exit-code dist/",
78
- "bun run lint",
79
- "bun run typecheck",
80
- "bun run test",
81
- "./node_modules/.bin/commitlint"
82
- ]
90
+ "test": "bun scripts/test.ts"
83
91
  },
84
92
  "peerDependencies": {
85
93
  "@swc/core": "1.16.2",
@@ -94,6 +102,7 @@
94
102
  "@oxlint/plugins": "1.83.0",
95
103
  "@swc/core": "1.16.2",
96
104
  "@types/bun": "^1.3.0",
105
+ "ajv": "8.20.0",
97
106
  "dependency-cruiser": "18.4.0",
98
107
  "effect": "4.0.0-rc.115",
99
108
  "oxlint": "1.83.0",
@@ -0,0 +1,8 @@
1
+ {
2
+ "diagnosticSeverity": {
3
+ "nodeBuiltinImport": "error",
4
+ "asyncFunction": "error",
5
+ "newPromise": "error",
6
+ "extendsNativeError": "error"
7
+ }
8
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "plugins": ["node", "promise", "unicorn"],
3
+ "rules": {
4
+ "node/no-sync": "error",
5
+ "oxc/no-async-await": "error",
6
+ "promise/avoid-new": "error",
7
+ "unicorn/no-process-exit": "error",
8
+ "effect-channel/no-throw": "error",
9
+ "effect-channel/no-try-catch": "error"
10
+ }
11
+ }
@@ -0,0 +1,197 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "type": "object",
4
+ "properties": {
5
+ "$schema": {
6
+ "type": "string"
7
+ },
8
+ "defaultBranch": {
9
+ "type": "string",
10
+ "minLength": 1,
11
+ "description": "The branch pull requests merge into; main when absent"
12
+ },
13
+ "gates": {
14
+ "type": "object",
15
+ "properties": {
16
+ "ci": {
17
+ "type": "array",
18
+ "prefixItems": [
19
+ {
20
+ "$ref": "#/$defs/Command"
21
+ }
22
+ ],
23
+ "minItems": 1,
24
+ "items": {
25
+ "$ref": "#/$defs/Command"
26
+ },
27
+ "description": "The commands CI runs on every pull request to the default branch, each one plain command"
28
+ },
29
+ "scheduled": {
30
+ "type": "array",
31
+ "items": {
32
+ "$ref": "#/$defs/Command"
33
+ },
34
+ "description": "The commands a cron-scheduled workflow runs"
35
+ },
36
+ "lint": {
37
+ "type": "array",
38
+ "items": {
39
+ "type": "string",
40
+ "enum": [
41
+ "checks-lint-coverage",
42
+ "checks-test-layout",
43
+ "checks-commit-identity",
44
+ "checks-comment-gate",
45
+ "checks-suppressions-ratchet",
46
+ "checks-ci-wiring",
47
+ "checks-quality"
48
+ ]
49
+ },
50
+ "allOf": [
51
+ {
52
+ "contains": {
53
+ "const": "checks-commit-identity"
54
+ }
55
+ },
56
+ {
57
+ "contains": {
58
+ "const": "checks-comment-gate"
59
+ }
60
+ },
61
+ {
62
+ "contains": {
63
+ "const": "checks-suppressions-ratchet"
64
+ }
65
+ },
66
+ {
67
+ "contains": {
68
+ "const": "checks-ci-wiring"
69
+ }
70
+ }
71
+ ]
72
+ }
73
+ },
74
+ "additionalProperties": false
75
+ },
76
+ "commitIdentity": {
77
+ "type": "object",
78
+ "properties": {
79
+ "authors": {
80
+ "type": "array",
81
+ "prefixItems": [
82
+ {
83
+ "$ref": "#/$defs/Identity"
84
+ }
85
+ ],
86
+ "minItems": 1,
87
+ "items": {
88
+ "$ref": "#/$defs/Identity"
89
+ },
90
+ "description": "The identities allowed to author and commit, in place of the kit's default owner"
91
+ }
92
+ },
93
+ "required": [
94
+ "authors"
95
+ ],
96
+ "additionalProperties": false
97
+ },
98
+ "sources": {
99
+ "type": "object",
100
+ "properties": {
101
+ "production": {
102
+ "type": "array",
103
+ "items": {
104
+ "$ref": "#/$defs/PathGlob"
105
+ },
106
+ "description": "The source the repository ships, as against tests and tooling"
107
+ },
108
+ "effect": {
109
+ "type": "object",
110
+ "properties": {
111
+ "paths": {
112
+ "type": "array",
113
+ "prefixItems": [
114
+ {
115
+ "$ref": "#/$defs/PathGlob"
116
+ }
117
+ ],
118
+ "minItems": 1,
119
+ "items": {
120
+ "$ref": "#/$defs/PathGlob"
121
+ },
122
+ "description": "Where source is written in Effect, held to the Effect rules of oxlint and the language service"
123
+ },
124
+ "exempt": {
125
+ "type": "array",
126
+ "items": {
127
+ "$ref": "#/$defs/PathGlob"
128
+ },
129
+ "description": "Files under paths the Effect rules pass over"
130
+ }
131
+ },
132
+ "required": [
133
+ "paths"
134
+ ],
135
+ "additionalProperties": false
136
+ }
137
+ },
138
+ "additionalProperties": false
139
+ },
140
+ "agentRules": {
141
+ "type": "object",
142
+ "properties": {
143
+ "on": {
144
+ "type": "array",
145
+ "items": {
146
+ "$ref": "#/$defs/RuleName"
147
+ },
148
+ "description": "Catalogued Rules switched on here"
149
+ },
150
+ "off": {
151
+ "type": "array",
152
+ "items": {
153
+ "$ref": "#/$defs/RuleName"
154
+ },
155
+ "description": "Catalogued Rules switched off here"
156
+ }
157
+ },
158
+ "additionalProperties": false
159
+ }
160
+ },
161
+ "additionalProperties": false,
162
+ "title": "quality.json",
163
+ "description": "What a repository has opted into from @avi2dg/checks, read by its bins and agent Rule selection",
164
+ "$defs": {
165
+ "Command": {
166
+ "type": "string",
167
+ "minLength": 1
168
+ },
169
+ "Identity": {
170
+ "type": "object",
171
+ "properties": {
172
+ "name": {
173
+ "type": "string",
174
+ "minLength": 1
175
+ },
176
+ "email": {
177
+ "type": "string",
178
+ "minLength": 1
179
+ }
180
+ },
181
+ "required": [
182
+ "name",
183
+ "email"
184
+ ],
185
+ "additionalProperties": false
186
+ },
187
+ "PathGlob": {
188
+ "type": "string",
189
+ "pattern": "^(?!\\.\\.?(?:\\/|$))(?:\\*\\*|(?:[\\w.@+-]|\\*(?!\\*))+)(?:\\/(?!\\.\\.?(?:\\/|$))(?:\\*\\*|(?:[\\w.@+-]|\\*(?!\\*))+))*\\/(?:[\\w.@+-]|\\*(?!\\*))*\\.\\w+$",
190
+ "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 ./"
191
+ },
192
+ "RuleName": {
193
+ "type": "string",
194
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
195
+ }
196
+ }
197
+ }
@@ -2,15 +2,8 @@
2
2
  import { Console, Effect, FileSystem, Path, Schema } from "effect";
3
3
  import { git } from "./git.ts";
4
4
  import { runMain } from "./main.ts";
5
- import {
6
- DEFAULT_BRANCH,
7
- ENTRY_POINT,
8
- EVERY_REPOSITORY,
9
- KIT_GATES,
10
- LintWiring,
11
- selectedGates,
12
- type KitGate,
13
- } from "./gates.ts";
5
+ import { DEFAULT_BRANCH, ENTRY_POINT, EVERY_REPOSITORY, KIT_GATES, QUALITY_FILE, selectedGates, type KitGate } from "./gates.ts";
6
+ import { readQuality, type Quality } from "./quality-file.ts";
14
7
 
15
8
  export type Command = readonly string[];
16
9
 
@@ -21,6 +14,7 @@ export type Gate = {
21
14
 
22
15
  export type Declaration = {
23
16
  readonly gates: readonly Gate[];
17
+ readonly scheduled: readonly Gate[];
24
18
  readonly defaultBranch: string;
25
19
  readonly lintGates: readonly KitGate[];
26
20
  };
@@ -53,6 +47,8 @@ type RunStep = {
53
47
  readonly script: string;
54
48
  };
55
49
 
50
+ type Trigger = (workflow: Workflow) => string | undefined;
51
+
56
52
  export class WiringError extends Schema.TaggedError<WiringError>()("WiringError", {
57
53
  message: Schema.String,
58
54
  }) {}
@@ -161,7 +157,7 @@ function selects(patterns: readonly string[], branch: string): boolean {
161
157
  return selected;
162
158
  }
163
159
 
164
- function triggerBlocker(workflow: Workflow, branch: string): string | undefined {
160
+ const pullRequestTrigger = (branch: string): Trigger => (workflow) => {
165
161
  const on = isRecord(workflow.document) ? workflow.document["on"] : undefined;
166
162
  const events = isRecord(on) ? Object.keys(on) : (names(on) ?? []);
167
163
  if (!events.includes("pull_request")) return `${workflow.path} does not trigger on pull_request`;
@@ -183,7 +179,14 @@ function triggerBlocker(workflow: Workflow, branch: string): string | undefined
183
179
  const missing = types === undefined ? [] : GATING_TYPES.filter((type) => !types.includes(type));
184
180
  if (missing.length > 0) return `${workflow.path} limits pull_request to types without ${missing.join(", ")}`;
185
181
  return undefined;
186
- }
182
+ };
183
+
184
+ const scheduleTrigger: Trigger = (workflow) => {
185
+ const on = isRecord(workflow.document) ? workflow.document["on"] : undefined;
186
+ const schedule = isRecord(on) ? on["schedule"] : undefined;
187
+ const crons = Array.isArray(schedule) ? schedule.filter((entry) => isRecord(entry) && typeof entry["cron"] === "string") : [];
188
+ return crons.length > 0 ? undefined : `${workflow.path} does not trigger on a schedule`;
189
+ };
187
190
 
188
191
  // GitHub prefixes any other if: with success(), so only a status function overrides a needed job's skip.
189
192
  function skippedBy(jobs: Readonly<Record<string, unknown>>, id: string, seen: readonly string[]): string | undefined {
@@ -202,7 +205,7 @@ function localCall(uses: unknown): string | undefined {
202
205
  return typeof uses === "string" && uses.startsWith("./") ? uses.slice(2) : undefined;
203
206
  }
204
207
 
205
- function runSteps(workflows: readonly Workflow[], branch: string): readonly RunStep[] {
208
+ function runSteps(workflows: readonly Workflow[], trigger: Trigger): readonly RunStep[] {
206
209
  const documents = new Map(workflows.map((workflow) => [workflow.path, workflow.document]));
207
210
  const steps: RunStep[] = [];
208
211
 
@@ -240,7 +243,7 @@ function runSteps(workflows: readonly Workflow[], branch: string): readonly RunS
240
243
  };
241
244
 
242
245
  for (const workflow of workflows) {
243
- visit(workflow.document, workflow.path, triggerBlocker(workflow, branch), [workflow.path]);
246
+ visit(workflow.document, workflow.path, trigger(workflow), [workflow.path]);
244
247
  }
245
248
  return steps;
246
249
  }
@@ -250,10 +253,9 @@ function entryPointCommand(gate: Command, lintGates: readonly KitGate[]): Comman
250
253
  return index === -1 ? undefined : [...gate.slice(0, index), ENTRY_POINT.bin];
251
254
  }
252
255
 
253
- export function findGaps(declaration: Declaration, workflows: readonly Workflow[]): readonly Gap[] {
254
- const steps = runSteps(workflows, declaration.defaultBranch);
255
- return declaration.gates.flatMap((gate) => {
256
- const entryPoint = entryPointCommand(gate.words, declaration.lintGates);
256
+ function gapsAmong(gates: readonly Gate[], steps: readonly RunStep[], lintGates: readonly KitGate[]): readonly Gap[] {
257
+ return gates.flatMap((gate) => {
258
+ const entryPoint = entryPointCommand(gate.words, lintGates);
257
259
  const commands = [
258
260
  { command: gate.command, words: gate.words },
259
261
  ...(entryPoint === undefined ? [] : [{ command: entryPoint.join(" "), words: entryPoint }]),
@@ -274,10 +276,16 @@ export function findGaps(declaration: Declaration, workflows: readonly Workflow[
274
276
  });
275
277
  }
276
278
 
277
- export function formatReport(declaration: Declaration, gaps: readonly Gap[]): string {
278
- const target = `pull requests to ${declaration.defaultBranch}`;
279
- if (gaps.length === 0) return `ci-wiring: ${declaration.gates.length} gate(s) run on ${target}`;
280
- const lines = [`ci-wiring: ${gaps.length} of ${declaration.gates.length} gate(s) do not run on ${target}:`];
279
+ export function findGaps(declaration: Declaration, workflows: readonly Workflow[]): readonly Gap[] {
280
+ return gapsAmong(declaration.gates, runSteps(workflows, pullRequestTrigger(declaration.defaultBranch)), declaration.lintGates);
281
+ }
282
+
283
+ export function findScheduledGaps(declaration: Declaration, workflows: readonly Workflow[]): readonly Gap[] {
284
+ return gapsAmong(declaration.scheduled, runSteps(workflows, scheduleTrigger), declaration.lintGates);
285
+ }
286
+
287
+ function gapLines(gaps: readonly Gap[]): readonly string[] {
288
+ const lines: string[] = [];
281
289
  for (const gap of gaps) {
282
290
  lines.push(` ${gap.gate}`);
283
291
  if (gap.blocked.length === 0) {
@@ -285,38 +293,48 @@ export function formatReport(declaration: Declaration, gaps: readonly Gap[]): st
285
293
  }
286
294
  for (const { location, blocker } of gap.blocked) lines.push(` ${location}: ${blocker}`);
287
295
  }
288
- return lines.join("\n");
296
+ return lines;
297
+ }
298
+
299
+ export function formatReport(declaration: Declaration, gaps: readonly Gap[]): string {
300
+ const target = `pull requests to ${declaration.defaultBranch}`;
301
+ if (gaps.length === 0) return `ci-wiring: ${declaration.gates.length} gate(s) run on ${target}`;
302
+ return [`ci-wiring: ${gaps.length} of ${declaration.gates.length} gate(s) do not run on ${target}:`, ...gapLines(gaps)].join("\n");
289
303
  }
290
304
 
291
- const decodeWiring = Schema.decodeUnknownEffect(LintWiring);
305
+ export function formatScheduledReport(declaration: Declaration, gaps: readonly Gap[]): string {
306
+ const total = declaration.scheduled.length;
307
+ if (gaps.length === 0) return `ci-wiring: ${total} scheduled command(s) run on a schedule`;
308
+ return [`ci-wiring: ${gaps.length} of ${total} scheduled command(s) do not run on a schedule:`, ...gapLines(gaps)].join("\n");
309
+ }
292
310
 
293
- export const parseDeclaration = Effect.fnUntraced(function* (
294
- manifest: unknown,
295
- source: string,
296
- ): Effect.fn.Return<Declaration, WiringError> {
297
- const configured = isRecord(manifest) && isRecord(manifest["ciWiring"]) ? manifest["ciWiring"] : {};
298
- const listed: unknown = configured["gates"];
299
- if (!Array.isArray(listed) || listed.length === 0) {
300
- return yield* new WiringError({ message: `${source} sets no ciWiring.gates, a non-empty array of commands` });
301
- }
302
- const commands: readonly unknown[] = listed;
311
+ const plainCommands = Effect.fnUntraced(function* (
312
+ listed: readonly string[],
313
+ subject: string,
314
+ ): Effect.fn.Return<readonly Gate[], WiringError> {
303
315
  const gates: Gate[] = [];
304
- for (const command of commands) {
305
- const words = typeof command === "string" ? plainCommand(command) : undefined;
306
- if (typeof command !== "string" || words === undefined) {
307
- return yield* new WiringError({
308
- message: `${source} ciWiring gate ${JSON.stringify(command)} is not one plain command`,
309
- });
316
+ for (const command of listed) {
317
+ const words = plainCommand(command);
318
+ if (words === undefined) {
319
+ return yield* new WiringError({ message: `${subject} ${JSON.stringify(command)} is not one plain command` });
310
320
  }
311
321
  gates.push({ command, words });
312
322
  }
313
- const { ciWiring } = yield* decodeWiring(manifest).pipe(
314
- Effect.mapError((cause) => new WiringError({ message: `${source}: ${cause.message}` })),
315
- );
323
+ return gates;
324
+ });
325
+
326
+ export const parseDeclaration = Effect.fnUntraced(function* (
327
+ { defaultBranch, gates: declared }: Quality,
328
+ source: string,
329
+ ): Effect.fn.Return<Declaration, WiringError> {
330
+ if (declared?.ci === undefined) {
331
+ return yield* new WiringError({ message: `${QUALITY_FILE} declares no gates.ci, a non-empty array of commands` });
332
+ }
316
333
  return {
317
- gates,
318
- defaultBranch: ciWiring?.defaultBranch ?? DEFAULT_BRANCH,
319
- lintGates: selectedGates(ciWiring?.lintGates),
334
+ gates: yield* plainCommands(declared.ci, `${source} gate`),
335
+ scheduled: yield* plainCommands(declared.scheduled ?? [], `${source} scheduled command`),
336
+ defaultBranch: defaultBranch ?? DEFAULT_BRANCH,
337
+ lintGates: selectedGates(declared.lint),
320
338
  };
321
339
  });
322
340
 
@@ -345,10 +363,10 @@ export function formatOmissions(lintGates: readonly KitGate[], omissions: readon
345
363
  if (omitted.length === 0) return undefined;
346
364
  if (omissions.length === 0) {
347
365
  const bins = omitted.map((gate) => gate.bin).join(", ");
348
- return `ci-wiring: ciWiring.lintGates leaves out ${bins}, none of which this repository's contents make applicable`;
366
+ return `ci-wiring: ${QUALITY_FILE} gates.lint leaves out ${bins}, none of which this repository's contents make applicable`;
349
367
  }
350
368
  return [
351
- `ci-wiring: ciWiring.lintGates leaves out ${omissions.length} gate(s) this repository's contents make applicable:`,
369
+ `ci-wiring: ${QUALITY_FILE} gates.lint leaves out ${omissions.length} gate(s) this repository's contents make applicable:`,
352
370
  ...omissions.map(({ gate, content, files }) => ` ${gate}: the repository tracks ${content} (${sample(files)})`),
353
371
  ].join("\n");
354
372
  }
@@ -359,16 +377,9 @@ export const parseWorkflow = (path: string, text: string): Effect.Effect<Workflo
359
377
  catch: (error) => new WiringError({ message: `cannot parse ${path}: ${String(error)}` }),
360
378
  });
361
379
 
362
- const parseJson = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown));
363
-
364
380
  export const readDeclaration = Effect.fn("readDeclaration")(function* (root: string) {
365
- const fs = yield* FileSystem.FileSystem;
366
- const path = (yield* Path.Path).join(root, "package.json");
367
- const manifest = yield* fs.readFileString(path).pipe(
368
- Effect.flatMap(parseJson),
369
- Effect.mapError((cause) => new WiringError({ message: `cannot read ${path} as JSON: ${cause.message}` })),
370
- );
371
- return yield* parseDeclaration(manifest, path);
381
+ const { source, quality } = yield* readQuality(root);
382
+ return yield* parseDeclaration(quality, source);
372
383
  });
373
384
 
374
385
  export const readWorkflows = Effect.fn("readWorkflows")(function* (root: string) {
@@ -389,7 +400,8 @@ export const readWorkflows = Effect.fn("readWorkflows")(function* (root: string)
389
400
  const wiring = Effect.gen(function* () {
390
401
  const root = (yield* git(["rev-parse", "--show-toplevel"])).trim();
391
402
  const declaration = yield* readDeclaration(root);
392
- const gaps = findGaps(declaration, yield* readWorkflows(root));
403
+ const workflows = yield* readWorkflows(root);
404
+ const gaps = findGaps(declaration, workflows);
393
405
  const omissions = yield* findOmissions(root, declaration.lintGates);
394
406
  const gapReport = formatReport(declaration, gaps);
395
407
  yield* gaps.length > 0 ? Console.error(gapReport) : Console.log(gapReport);
@@ -397,7 +409,11 @@ const wiring = Effect.gen(function* () {
397
409
  if (omissionReport !== undefined) {
398
410
  yield* omissions.length > 0 ? Console.error(omissionReport) : Console.log(omissionReport);
399
411
  }
400
- return gaps.length === 0 && omissions.length === 0;
412
+ if (declaration.scheduled.length === 0) return gaps.length === 0 && omissions.length === 0;
413
+ const scheduledGaps = findScheduledGaps(declaration, workflows);
414
+ const scheduledReport = formatScheduledReport(declaration, scheduledGaps);
415
+ yield* scheduledGaps.length > 0 ? Console.error(scheduledReport) : Console.log(scheduledReport);
416
+ return gaps.length === 0 && omissions.length === 0 && scheduledGaps.length === 0;
401
417
  });
402
418
 
403
419
  if (import.meta.main) runMain("ci-wiring", wiring);
@@ -1,10 +1,8 @@
1
1
  #!/usr/bin/env bun
2
- import { Console, Effect, FileSystem, Path, Schema } from "effect";
2
+ import { Console, Effect, Schema } from "effect";
3
3
  import { git } from "./git.ts";
4
4
  import { runMain, Usage } from "./main.ts";
5
-
6
- const Identity = Schema.Struct({ name: Schema.String, email: Schema.String });
7
- type Identity = typeof Identity.Type;
5
+ import { readQuality, type Identity } from "./quality-file.ts";
8
6
 
9
7
  type Commit = {
10
8
  readonly sha: string;
@@ -32,16 +30,6 @@ const RECORD = "\u001e";
32
30
 
33
31
  const USAGE = "usage: commit-identity.ts <ref> | <base-ref> <head-ref>";
34
32
 
35
- const Manifest = Schema.Struct({
36
- commitIdentity: Schema.optional(Schema.Struct({ authors: Schema.NonEmptyArray(Identity) })),
37
- });
38
- const parseJson = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown));
39
- const decodeManifest = Schema.decodeUnknownEffect(Manifest);
40
-
41
- class InvalidAllowlist extends Schema.TaggedError<InvalidAllowlist>()("InvalidAllowlist", {
42
- message: Schema.String,
43
- }) {}
44
-
45
33
  class UnreadableLog extends Schema.TaggedError<UnreadableLog>()("UnreadableLog", {
46
34
  message: Schema.String,
47
35
  }) {}
@@ -51,25 +39,8 @@ function render(identity: Identity): string {
51
39
  }
52
40
 
53
41
  const allowedAuthors = Effect.gen(function* () {
54
- const fs = yield* FileSystem.FileSystem;
55
- const path = yield* Path.Path;
56
- const root = (yield* git(["rev-parse", "--show-toplevel"])).trim();
57
- const manifestPath = path.join(root, "package.json");
58
- if (!(yield* fs.exists(manifestPath))) return DEFAULT_AUTHORS;
59
-
60
- const parsed = yield* fs.readFileString(manifestPath).pipe(
61
- Effect.flatMap(parseJson),
62
- Effect.mapError((cause) => new InvalidAllowlist({ message: `cannot read ${manifestPath} as JSON: ${cause.message}` })),
63
- );
64
- const manifest = yield* decodeManifest(parsed).pipe(
65
- Effect.mapError(
66
- () =>
67
- new InvalidAllowlist({
68
- message: `${manifestPath} sets commitIdentity without a non-empty authors array of {name, email}`,
69
- }),
70
- ),
71
- );
72
- return manifest.commitIdentity?.authors ?? DEFAULT_AUTHORS;
42
+ const { quality } = yield* readQuality((yield* git(["rev-parse", "--show-toplevel"])).trim());
43
+ return quality.commitIdentity?.authors ?? DEFAULT_AUTHORS;
73
44
  });
74
45
 
75
46
  const readCommits = Effect.fn("readCommits")(function* (revisions: readonly string[]) {