@avi2dg/checks 0.24.1 → 0.25.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 (60) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +27 -19
  3. package/dist/effect-channel/index.js +4 -4
  4. package/dist/readability/index.js +5 -5
  5. package/docs/configs/commit-messages.md +1 -1
  6. package/docs/configs/native-settings.md +3 -2
  7. package/docs/design.md +9 -3
  8. package/docs/gates/checks-changelog.md +5 -1
  9. package/docs/gates/checks-comment-gate.md +3 -4
  10. package/docs/gates/checks-docs.md +5 -5
  11. package/docs/gates/checks-flake.md +1 -1
  12. package/docs/gates/checks-lint.md +1 -1
  13. package/docs/gates/checks-quarantine-clock.md +1 -1
  14. package/docs/gates/checks-test-layout.md +14 -9
  15. package/docs/gates/checks-test.md +1 -1
  16. package/docs/gates/checks-unused.md +65 -0
  17. package/knip-base.json +4 -0
  18. package/package.json +68 -104
  19. package/{scripts → src/complexity}/repetition.ts +2 -2
  20. package/{scripts → src/complexity}/suppressions-ratchet.ts +2 -2
  21. package/src/complexity/unused.ts +97 -0
  22. package/src/core/gates.ts +45 -0
  23. package/{scripts → src/core}/lint.ts +2 -2
  24. package/{scripts → src/delivery}/changelog-write.ts +30 -6
  25. package/{scripts → src/delivery}/changelog.ts +14 -4
  26. package/{scripts → src/delivery}/ci-wiring.ts +3 -3
  27. package/{scripts → src/delivery}/commit-identity.ts +2 -2
  28. package/{scripts → src/delivery}/release-notes.ts +1 -1
  29. package/{scripts → src/delivery}/release-report.ts +2 -2
  30. package/{scripts → src/dependencies}/vendor-args.ts +1 -1
  31. package/{scripts → src/dependencies}/vendor.ts +2 -2
  32. package/{scripts → src/docs}/doc-snapshot.ts +1 -1
  33. package/{scripts → src/docs}/doc-templates.ts +1 -1
  34. package/{scripts → src/docs}/docs.ts +2 -2
  35. package/{scripts → src/quality}/comment-gate.ts +2 -2
  36. package/{scripts → src/quality}/comment-matchers.ts +1 -1
  37. package/{scripts → src/testing}/flake.ts +1 -1
  38. package/{scripts → src/testing}/mutation-compare.ts +1 -1
  39. package/{scripts → src/testing}/quarantine-clock.ts +2 -2
  40. package/{scripts → src/testing}/subsumed-tests.ts +2 -2
  41. package/{scripts → src/testing}/test-layout.ts +47 -14
  42. package/{scripts → src/testing}/test-skips.ts +1 -1
  43. package/{scripts → src/testing}/test.ts +2 -2
  44. package/templates/changelog.md +2 -0
  45. package/CONTRIBUTING.md +0 -93
  46. package/docs/gates/checks-backtest.md +0 -54
  47. package/scripts/backtest.ts +0 -122
  48. package/scripts/gates.ts +0 -37
  49. package/scripts/range-gate.ts +0 -8
  50. /package/{scripts → src/core}/git.ts +0 -0
  51. /package/{scripts → src/core}/main.ts +0 -0
  52. /package/{scripts → src/core}/swc.ts +0 -0
  53. /package/{scripts → src/delivery}/shell-command.ts +0 -0
  54. /package/{scripts → src/docs}/doc-outline.ts +0 -0
  55. /package/{scripts → src/docs}/doc-references.ts +0 -0
  56. /package/{scripts → src/docs}/doc-rules.ts +0 -0
  57. /package/{scripts → src/docs}/prose-matchers.ts +0 -0
  58. /package/{scripts → src/quality}/comments.ts +0 -0
  59. /package/{scripts → src/quality}/lint-coverage.sh +0 -0
  60. /package/{scripts → src/testing}/test-report.ts +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avi2dg/checks",
3
- "version": "0.24.1",
3
+ "version": "0.25.0",
4
4
  "description": "Deterministic checks shared across the captain's TypeScript repos",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -13,50 +13,49 @@
13
13
  "type": "module",
14
14
  "files": [
15
15
  "CHANGELOG.md",
16
- "CONTRIBUTING.md",
17
16
  "docs/configs/",
18
17
  "docs/design.md",
19
18
  "docs/gates/",
20
19
  "bunfig.toml",
21
20
  "commitlint.config.js",
22
21
  "dependency-cruiser.config.js",
23
- "scripts/lint-coverage.sh",
24
- "scripts/main.ts",
25
- "scripts/git.ts",
26
- "scripts/gates.ts",
27
- "scripts/lint.ts",
28
- "scripts/changelog.ts",
29
- "scripts/changelog-write.ts",
30
- "scripts/release-notes.ts",
31
- "scripts/release-report.ts",
32
- "scripts/test-layout.ts",
33
- "scripts/test.ts",
34
- "scripts/test-skips.ts",
35
- "scripts/swc.ts",
36
- "scripts/test-report.ts",
37
- "scripts/flake.ts",
38
- "scripts/commit-identity.ts",
39
- "scripts/mutation-compare.ts",
40
- "scripts/subsumed-tests.ts",
41
- "scripts/ci-wiring.ts",
42
- "scripts/shell-command.ts",
43
- "scripts/comment-matchers.ts",
44
- "scripts/prose-matchers.ts",
45
- "scripts/comments.ts",
46
- "scripts/comment-gate.ts",
47
- "scripts/suppressions-ratchet.ts",
48
- "scripts/backtest.ts",
49
- "scripts/range-gate.ts",
50
- "scripts/repetition.ts",
51
- "scripts/quarantine-clock.ts",
52
- "scripts/docs.ts",
53
- "scripts/vendor.ts",
54
- "scripts/vendor-args.ts",
55
- "scripts/doc-outline.ts",
56
- "scripts/doc-rules.ts",
57
- "scripts/doc-references.ts",
58
- "scripts/doc-snapshot.ts",
59
- "scripts/doc-templates.ts",
22
+ "knip-base.json",
23
+ "src/quality/lint-coverage.sh",
24
+ "src/core/main.ts",
25
+ "src/core/git.ts",
26
+ "src/core/gates.ts",
27
+ "src/core/lint.ts",
28
+ "src/delivery/changelog.ts",
29
+ "src/delivery/changelog-write.ts",
30
+ "src/delivery/release-notes.ts",
31
+ "src/delivery/release-report.ts",
32
+ "src/testing/test-layout.ts",
33
+ "src/testing/test.ts",
34
+ "src/testing/test-skips.ts",
35
+ "src/core/swc.ts",
36
+ "src/testing/test-report.ts",
37
+ "src/testing/flake.ts",
38
+ "src/delivery/commit-identity.ts",
39
+ "src/testing/mutation-compare.ts",
40
+ "src/testing/subsumed-tests.ts",
41
+ "src/delivery/ci-wiring.ts",
42
+ "src/delivery/shell-command.ts",
43
+ "src/quality/comment-matchers.ts",
44
+ "src/docs/prose-matchers.ts",
45
+ "src/quality/comments.ts",
46
+ "src/quality/comment-gate.ts",
47
+ "src/complexity/suppressions-ratchet.ts",
48
+ "src/complexity/repetition.ts",
49
+ "src/complexity/unused.ts",
50
+ "src/testing/quarantine-clock.ts",
51
+ "src/docs/docs.ts",
52
+ "src/dependencies/vendor.ts",
53
+ "src/dependencies/vendor-args.ts",
54
+ "src/docs/doc-outline.ts",
55
+ "src/docs/doc-rules.ts",
56
+ "src/docs/doc-references.ts",
57
+ "src/docs/doc-snapshot.ts",
58
+ "src/docs/doc-templates.ts",
60
59
  "templates/",
61
60
  "presets/effect.oxlint.json",
62
61
  "presets/effect.language-service.json",
@@ -67,77 +66,41 @@
67
66
  "dist/readability/index.js"
68
67
  ],
69
68
  "exports": {
70
- "./bunfig.toml": "./bunfig.toml",
71
- "./commitlint.config.js": "./commitlint.config.js",
72
69
  "./dependency-cruiser.config.js": "./dependency-cruiser.config.js",
73
- "./scripts/lint-coverage.sh": "./scripts/lint-coverage.sh",
74
- "./scripts/lint.ts": "./scripts/lint.ts",
75
- "./scripts/changelog.ts": "./scripts/changelog.ts",
76
- "./scripts/changelog-write.ts": "./scripts/changelog-write.ts",
77
- "./scripts/release-notes.ts": "./scripts/release-notes.ts",
78
- "./scripts/release-report.ts": "./scripts/release-report.ts",
79
- "./scripts/test-layout.ts": "./scripts/test-layout.ts",
80
- "./scripts/test.ts": "./scripts/test.ts",
81
- "./scripts/test-skips.ts": "./scripts/test-skips.ts",
82
- "./scripts/flake.ts": "./scripts/flake.ts",
83
- "./scripts/commit-identity.ts": "./scripts/commit-identity.ts",
84
- "./scripts/mutation-compare.ts": "./scripts/mutation-compare.ts",
85
- "./scripts/subsumed-tests.ts": "./scripts/subsumed-tests.ts",
86
- "./scripts/ci-wiring.ts": "./scripts/ci-wiring.ts",
87
- "./scripts/comment-matchers.ts": "./scripts/comment-matchers.ts",
88
- "./scripts/prose-matchers.ts": "./scripts/prose-matchers.ts",
89
- "./scripts/comments.ts": "./scripts/comments.ts",
90
- "./scripts/comment-gate.ts": "./scripts/comment-gate.ts",
91
- "./scripts/suppressions-ratchet.ts": "./scripts/suppressions-ratchet.ts",
92
- "./scripts/backtest.ts": "./scripts/backtest.ts",
93
- "./scripts/repetition.ts": "./scripts/repetition.ts",
94
- "./scripts/quarantine-clock.ts": "./scripts/quarantine-clock.ts",
95
- "./scripts/docs.ts": "./scripts/docs.ts",
96
- "./scripts/vendor.ts": "./scripts/vendor.ts",
97
- "./templates/readme.md": "./templates/readme.md",
98
- "./templates/changelog.md": "./templates/changelog.md",
99
- "./templates/adr.md": "./templates/adr.md",
100
- "./templates/agents.md": "./templates/agents.md",
101
- "./templates/claude.md": "./templates/claude.md",
102
- "./templates/tutorial.md": "./templates/tutorial.md",
103
- "./templates/how-to.md": "./templates/how-to.md",
104
- "./templates/reference.md": "./templates/reference.md",
105
- "./templates/explanation.md": "./templates/explanation.md",
106
- "./presets/effect.oxlint.json": "./presets/effect.oxlint.json",
107
- "./presets/effect.language-service.json": "./presets/effect.language-service.json",
108
- "./oxlintrc.json": "./oxlintrc.json",
70
+ "./knip-base.json": "./knip-base.json",
71
+ "./scripts/test-skips.ts": "./src/testing/test-skips.ts",
72
+ "./scripts/comment-matchers.ts": "./src/quality/comment-matchers.ts",
73
+ "./scripts/prose-matchers.ts": "./src/docs/prose-matchers.ts",
109
74
  "./stryker.preset.js": "./stryker.preset.js",
110
- "./tsconfig.effect.json": "./tsconfig.effect.json",
111
- "./dist/effect-channel/index.js": "./dist/effect-channel/index.js",
112
- "./dist/readability/index.js": "./dist/readability/index.js"
75
+ "./tsconfig.effect.json": "./tsconfig.effect.json"
113
76
  },
114
77
  "bin": {
115
- "checks-lint": "scripts/lint.ts",
116
- "checks-changelog": "scripts/changelog-write.ts",
117
- "checks-release-notes": "scripts/release-notes.ts",
118
- "checks-release-report": "scripts/release-report.ts",
119
- "checks-lint-coverage": "scripts/lint-coverage.sh",
120
- "checks-test-layout": "scripts/test-layout.ts",
121
- "checks-test": "scripts/test.ts",
122
- "checks-flake": "scripts/flake.ts",
123
- "checks-commit-identity": "scripts/commit-identity.ts",
124
- "checks-mutation-compare": "scripts/mutation-compare.ts",
125
- "checks-subsumed-tests": "scripts/subsumed-tests.ts",
126
- "checks-ci-wiring": "scripts/ci-wiring.ts",
127
- "checks-comment-gate": "scripts/comment-gate.ts",
128
- "checks-suppressions-ratchet": "scripts/suppressions-ratchet.ts",
129
- "checks-backtest": "scripts/backtest.ts",
130
- "checks-repetition": "scripts/repetition.ts",
131
- "checks-quarantine-clock": "scripts/quarantine-clock.ts",
132
- "checks-docs": "scripts/docs.ts",
133
- "checks-vendor": "scripts/vendor.ts"
78
+ "checks-lint": "src/core/lint.ts",
79
+ "checks-changelog": "src/delivery/changelog-write.ts",
80
+ "checks-release-notes": "src/delivery/release-notes.ts",
81
+ "checks-release-report": "src/delivery/release-report.ts",
82
+ "checks-lint-coverage": "src/quality/lint-coverage.sh",
83
+ "checks-test-layout": "src/testing/test-layout.ts",
84
+ "checks-test": "src/testing/test.ts",
85
+ "checks-flake": "src/testing/flake.ts",
86
+ "checks-commit-identity": "src/delivery/commit-identity.ts",
87
+ "checks-mutation-compare": "src/testing/mutation-compare.ts",
88
+ "checks-subsumed-tests": "src/testing/subsumed-tests.ts",
89
+ "checks-ci-wiring": "src/delivery/ci-wiring.ts",
90
+ "checks-comment-gate": "src/quality/comment-gate.ts",
91
+ "checks-suppressions-ratchet": "src/complexity/suppressions-ratchet.ts",
92
+ "checks-repetition": "src/complexity/repetition.ts",
93
+ "checks-unused": "src/complexity/unused.ts",
94
+ "checks-quarantine-clock": "src/testing/quarantine-clock.ts",
95
+ "checks-docs": "src/docs/docs.ts",
96
+ "checks-vendor": "src/dependencies/vendor.ts"
134
97
  },
135
98
  "scripts": {
136
- "prepare": "bun scripts/vendor.ts --library effect --package effect --repository https://github.com/Effect-TS/effect.git --tag 'effect@{version}' --path packages/effect/package.json",
137
- "build": "bun build effect-channel/index.ts --outdir dist/effect-channel --target node --format esm && bun build readability/index.ts --outdir dist/readability --target node --format esm && bun scripts/doc-templates-write.ts && bun scripts/changelog-write.ts && bun scripts/doc-blocks-write.ts",
138
- "lint": "oxlint --type-aware && bun scripts/lint.ts && depcruise --config .dependency-cruiser.cjs .",
99
+ "prepare": "bun src/dependencies/vendor.ts --library effect --package effect --repository https://github.com/Effect-TS/effect.git --tag 'effect@{version}' --path packages/effect/package.json",
100
+ "build": "bun build src/quality/effect-channel/index.ts --outdir dist/effect-channel --target node --format esm && bun build src/complexity/readability/index.ts --outdir dist/readability --target node --format esm && bun scripts/doc-templates-write.ts && bun src/delivery/changelog-write.ts && bun scripts/doc-blocks-write.ts",
101
+ "lint": "oxlint --type-aware && bun src/core/lint.ts && depcruise --config .dependency-cruiser.cjs .",
139
102
  "typecheck": "tsc --noEmit && effect-tsgo diagnostics --project tsconfig.json --format text --strict",
140
- "test": "bun scripts/test.ts"
103
+ "test": "bun src/testing/test.ts"
141
104
  },
142
105
  "peerDependencies": {
143
106
  "@swc/core": "1.16.2",
@@ -165,6 +128,7 @@
165
128
  "@commitlint/cli": "21.2.3",
166
129
  "@commitlint/config-conventional": "21.2.3",
167
130
  "@effect/platform-bun": "4.0.0-rc.115",
168
- "@effect/platform-node-shared": "4.0.0-rc.115"
131
+ "@effect/platform-node-shared": "4.0.0-rc.115",
132
+ "knip": "6.38.0"
169
133
  }
170
134
  }
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
  import { Console, Effect, FileSystem, Path, Schema } from "effect";
3
- import { changedPaths, checkoutFiles, collect, git, pathsAt, rangeFromArgs } from "./git.ts";
4
- import { runMain } from "./main.ts";
3
+ import { changedPaths, checkoutFiles, collect, git, pathsAt, rangeFromArgs } from "../core/git.ts";
4
+ import { runMain } from "../core/main.ts";
5
5
 
6
6
  export type Fragment = {
7
7
  readonly file: string;
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
  import { Console, Effect, Schema } from "effect";
3
- import { git, commitOf, parentOrEmptyTree, refArgs } from "./git.ts";
4
- import { runMain } from "./main.ts";
3
+ import { git, commitOf, parentOrEmptyTree, refArgs } from "../core/git.ts";
4
+ import { runMain } from "../core/main.ts";
5
5
 
6
6
  export const SUPPRESSIONS = "oxlint-suppressions.json";
7
7
 
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env bun
2
+ import { Console, Effect, FileSystem, Path, Schema } from "effect";
3
+ import { collect, git } from "../core/git.ts";
4
+ import { runMain } from "../core/main.ts";
5
+
6
+ export type Scan = { readonly tracked: number; readonly files: readonly string[] };
7
+
8
+ const NAME = "unused";
9
+ const CONFIGS = [
10
+ ".knip.json",
11
+ ".knip.jsonc",
12
+ "knip.json",
13
+ "knip.jsonc",
14
+ "knip.js",
15
+ "knip.ts",
16
+ "knip.config.js",
17
+ "knip.config.ts",
18
+ ] as const;
19
+ const TYPESCRIPT = ["*.ts", "*.tsx"];
20
+ const JUDGED = /\.tsx?$/;
21
+
22
+ class UnusedError extends Schema.TaggedError<UnusedError>()("UnusedError", {
23
+ message: Schema.String,
24
+ }) {}
25
+
26
+ const decodeKnipReport = Schema.decodeUnknownEffect(
27
+ Schema.fromJsonString(
28
+ Schema.Struct({
29
+ issues: Schema.Array(Schema.Struct({ files: Schema.optional(Schema.Array(Schema.Struct({ name: Schema.String }))) })),
30
+ }),
31
+ ),
32
+ );
33
+
34
+ export const filesOf = Effect.fn("filesOf")(function* (stdout: string) {
35
+ const { issues } = yield* decodeKnipReport(stdout).pipe(
36
+ Effect.mapError((cause) => new UnusedError({ message: `knip's JSON report does not decode: ${cause.message}` })),
37
+ );
38
+ return issues
39
+ .flatMap(({ files = [] }) => files.map(({ name }) => name))
40
+ .filter((file) => JUDGED.test(file))
41
+ .toSorted();
42
+ });
43
+
44
+ export function report({ tracked, files }: Scan): string {
45
+ if (files.length === 0) return `${NAME}: no unreferenced files among ${tracked} tracked .ts/.tsx file(s)`;
46
+ return [`${NAME}: ${files.length} unreferenced file(s):`, ...files.map((file) => ` ${file}`)].join("\n");
47
+ }
48
+
49
+ const hasConfig = Effect.fn("hasConfig")(function* (root: string) {
50
+ const fs = yield* FileSystem.FileSystem;
51
+ const path = yield* Path.Path;
52
+ for (const name of CONFIGS) {
53
+ if (yield* fs.exists(path.join(root, name))) return true;
54
+ }
55
+ const manifest: unknown = yield* fs.readFileString(path.join(root, "package.json")).pipe(
56
+ Effect.flatMap((text) =>
57
+ Effect.try({
58
+ try: () => JSON.parse(text) as unknown,
59
+ catch: () => new UnusedError({ message: "package.json does not parse as JSON" }),
60
+ })
61
+ ),
62
+ );
63
+ return typeof manifest === "object" && manifest !== null && "knip" in manifest;
64
+ });
65
+
66
+ const knip = Effect.fn("knip")(function* () {
67
+ const path = yield* Path.Path;
68
+ const main = yield* Effect.try({
69
+ try: () => new URL("../bin/knip.js", import.meta.resolve("knip")),
70
+ catch: () => new UnusedError({ message: "cannot resolve knip from the installed kit" }),
71
+ });
72
+ return yield* path.fromFileUrl(main);
73
+ });
74
+
75
+ const unused = Effect.gen(function* () {
76
+ const root = (yield* git(["rev-parse", "--show-toplevel"])).trim();
77
+ const tracked = (yield* git(["ls-files", "--", ...TYPESCRIPT], root))
78
+ .split("\n")
79
+ .map((line) => line.trim())
80
+ .filter((line) => line !== "");
81
+ if (tracked.length === 0) return yield* new UnusedError({ message: "no tracked .ts or .tsx files to scan" });
82
+ if (!(yield* hasConfig(root))) {
83
+ yield* Console.log(`${NAME}: no knip configuration names entry files, so add one extending the kit's knip-base.json`);
84
+ return false;
85
+ }
86
+ const run = yield* collect(process.execPath, [yield* knip(), "--files", "--reporter", "json"], root).pipe(
87
+ Effect.mapError((cause) => new UnusedError({ message: `cannot run knip: ${cause.message}` })),
88
+ );
89
+ if (run.exitCode !== 0 && run.exitCode !== 1) {
90
+ return yield* new UnusedError({ message: `knip exits ${run.exitCode}: ${(run.stdout + run.stderr).trim()}` });
91
+ }
92
+ const files = yield* filesOf(run.stdout);
93
+ yield* Console.log(report({ tracked: tracked.length, files }));
94
+ return files.length === 0;
95
+ });
96
+
97
+ if (import.meta.main) runMain(NAME, unused);
@@ -0,0 +1,45 @@
1
+ export type Program = {
2
+ readonly bin: string;
3
+ readonly script: string;
4
+ };
5
+
6
+ type TrackedContent = {
7
+ readonly pathspecs: readonly string[];
8
+ readonly content: string;
9
+ };
10
+
11
+ export const EVERY_REPOSITORY = "every repository";
12
+
13
+ export const VECTORS = ["complexity", "quality", "testing", "docs", "delivery", "dependencies"] as const;
14
+
15
+ export type Vector = (typeof VECTORS)[number];
16
+
17
+ export type KitGate = {
18
+ readonly bin: string;
19
+ readonly vector: Vector;
20
+ readonly file: string;
21
+ readonly reads: "tree" | "range";
22
+ readonly args?: readonly string[];
23
+ readonly appliesTo: typeof EVERY_REPOSITORY | TrackedContent;
24
+ };
25
+
26
+ export const ENTRY_POINT: Program = { bin: "checks-lint", script: "core/lint.ts" };
27
+
28
+ export const TEST_ENTRY_POINT: Program = { bin: "checks-test", script: "testing/test.ts" };
29
+
30
+ export const DEFAULT_BRANCH = "main";
31
+
32
+ const TYPESCRIPT_SOURCE: TrackedContent = { pathspecs: ["*.ts", "*.tsx"], content: "TypeScript source" };
33
+
34
+ export const KIT_GATES = [
35
+ { bin: "checks-lint-coverage", vector: "quality", file: "lint-coverage.sh", reads: "tree", appliesTo: TYPESCRIPT_SOURCE },
36
+ { bin: "checks-test-layout", vector: "testing", file: "test-layout.ts", reads: "tree", appliesTo: TYPESCRIPT_SOURCE },
37
+ { bin: "checks-commit-identity", vector: "delivery", file: "commit-identity.ts", reads: "range", appliesTo: EVERY_REPOSITORY },
38
+ { bin: "checks-comment-gate", vector: "quality", file: "comment-gate.ts", reads: "range", appliesTo: EVERY_REPOSITORY },
39
+ { bin: "checks-suppressions-ratchet", vector: "complexity", file: "suppressions-ratchet.ts", reads: "range", appliesTo: EVERY_REPOSITORY },
40
+ { bin: "checks-ci-wiring", vector: "delivery", file: "ci-wiring.ts", reads: "tree", appliesTo: EVERY_REPOSITORY },
41
+ { bin: "checks-docs", vector: "docs", file: "docs.ts", reads: "range", appliesTo: EVERY_REPOSITORY },
42
+ { bin: "checks-repetition", vector: "complexity", file: "repetition.ts", reads: "range", appliesTo: TYPESCRIPT_SOURCE },
43
+ { bin: "checks-unused", vector: "complexity", file: "unused.ts", reads: "tree", appliesTo: TYPESCRIPT_SOURCE },
44
+ { bin: "checks-quarantine-clock", vector: "testing", file: "quarantine-clock.ts", reads: "range", appliesTo: EVERY_REPOSITORY },
45
+ ] as const satisfies readonly KitGate[];
@@ -151,8 +151,8 @@ function outcomeOf(exitCode: number): Outcome {
151
151
 
152
152
  const runGate = Effect.fn("runGate")(function* (gate: KitGate, range: Range) {
153
153
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
154
- const script = (yield* Path.Path).join(import.meta.dir, gate.script);
155
- const program = gate.script.endsWith(".sh") ? "sh" : process.execPath;
154
+ const script = (yield* Path.Path).join(import.meta.dir, "..", gate.vector, gate.file);
155
+ const program = gate.file.endsWith(".sh") ? "sh" : process.execPath;
156
156
  const args = gate.reads === "range" ? [script, ...range.refs] : [script, ...(gate.args ?? [])];
157
157
  const exitCode = yield* spawner
158
158
  .exitCode(ChildProcess.make(program, args, { stdin: "ignore", stdout: "inherit", stderr: "inherit" }))
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env bun
2
2
  import { Console, DateTime, Effect, FileSystem, Path, Schema } from "effect";
3
- import { cuts, releaseDates, renderChangelog, type Bump, type Cut, type Release } from "./changelog.ts";
4
- import { git } from "./git.ts";
5
- import { runMain } from "./main.ts";
3
+ import { cuts, hasEntries, namesPullRequest, releaseDates, renderChangelog, type Bump, type Cut, type Release } from "./changelog.ts";
4
+ import { git } from "../core/git.ts";
5
+ import { runMain } from "../core/main.ts";
6
6
 
7
7
  const NAME = "checks-changelog";
8
8
  const CHANGELOG = "CHANGELOG.md";
@@ -69,8 +69,28 @@ const readPublished = Effect.fn("readPublished")(function* (root: string) {
69
69
  return new Set(tags.split("\n").flatMap((tag) => (tag.startsWith(TAG_PREFIX) ? [tag.slice(TAG_PREFIX.length)] : [])));
70
70
  });
71
71
 
72
- const subjectsOf = Effect.fn("subjectsOf")(function* (root: string, { version, date, through, after }: Cut) {
73
- const log = yield* git(["log", "--topo-order", "--format=%s", through, "--not", ...after], root);
72
+ const mergedTips = Effect.fn("mergedTips")(function* (root: string, through: string) {
73
+ // A bump that names its pull request or sits off the first-parent chain is main's release, so no squash merge of this branch releases more under it.
74
+ if (namesPullRequest((yield* git(["log", "-1", "--format=%s", through], root)).trim())) return [];
75
+ if (!(yield* git(["rev-list", "--first-parent", "HEAD"], root)).split("\n").includes(through)) return [];
76
+ const merges = yield* git(["log", "--merges", "--format=%P", `${through}..HEAD`], root);
77
+ const tips: string[] = [];
78
+ for (const [, ...parents] of merges.split("\n").filter((line) => line !== "").map((line) => line.split(" "))) {
79
+ for (const parent of parents) {
80
+ // A parent past the bump belongs to a later release, so only a parent beside it joins this one.
81
+ const past = yield* git(["merge-base", "--is-ancestor", through, parent], root).pipe(
82
+ Effect.as(true),
83
+ Effect.catchTag("GitFailure", () => Effect.succeed(false)),
84
+ );
85
+ if (!past) tips.push(parent);
86
+ }
87
+ }
88
+ return tips;
89
+ });
90
+
91
+ // The bump reaches no commit main gained after it, yet the squash merge releases them under it.
92
+ const subjectsOf = Effect.fn("subjectsOf")(function* (root: string, { version, date, through, after }: Cut, extra: readonly string[] = []) {
93
+ const log = yield* git(["log", "--topo-order", "--format=%s", through, ...extra, "--not", ...after], root);
74
94
  return { version, date, subjects: log.split("\n").filter((subject) => subject !== "") } satisfies Release;
75
95
  });
76
96
 
@@ -94,7 +114,11 @@ const write = Effect.gen(function* () {
94
114
  const recorded = (yield* fs.exists(target)) ? releaseDates(yield* fs.readFileString(target)) : new Map<string, string>();
95
115
  const pending = version === (yield* versionAt(root, "HEAD")) ? undefined : { sha: "HEAD", version, date: yield* today };
96
116
  const released = cuts(yield* readBumps(root), recorded, yield* readPublished(root), pending);
97
- const found = (yield* Effect.forEach(released, (cut) => subjectsOf(root, cut))).toReversed();
117
+ const tip = released.at(-1);
118
+ const extra = tip === undefined || tip.through === "HEAD" ? [] : yield* mergedTips(root, tip.through);
119
+ const found = (yield* Effect.forEach(released, (cut, index) => subjectsOf(root, cut, index === released.length - 1 ? extra : [])))
120
+ .filter(({ subjects }) => hasEntries(subjects))
121
+ .toReversed();
98
122
  yield* fs.writeFileString(target, renderChangelog(name, found, repositoryUrl));
99
123
  yield* Console.log(`${NAME}: wrote ${found.length} release(s) to ${CHANGELOG}`);
100
124
  return true;
@@ -1,6 +1,6 @@
1
- import { firstText, parseOutline, VERSION } from "./doc-outline.ts";
2
- import { RELEASED } from "./doc-rules.ts";
3
- import { CHANGE_GROUPS, type ChangeGroup } from "./doc-templates.ts";
1
+ import { firstText, parseOutline, VERSION } from "../docs/doc-outline.ts";
2
+ import { RELEASED } from "../docs/doc-rules.ts";
3
+ import { CHANGE_GROUPS, type ChangeGroup } from "../docs/doc-templates.ts";
4
4
 
5
5
  export type Bump = {
6
6
  readonly sha: string;
@@ -28,6 +28,8 @@ type Entry = {
28
28
 
29
29
  const CONVENTIONAL = /^([a-z]+)(?:\(([^()]*)\))?(!)?: (\S.*)$/;
30
30
 
31
+ const PULL_REQUEST = / \(#(\d+)\)$/;
32
+
31
33
  const GROUP_OF_TYPE = new Map<string, ChangeGroup>([
32
34
  ["feat", "Features"],
33
35
  ["fix", "Fixes"],
@@ -42,11 +44,15 @@ export function groupOf(subject: string): ChangeGroup | undefined {
42
44
  return breaking === "!" ? "Breaking changes" : GROUP_OF_TYPE.get(type);
43
45
  }
44
46
 
47
+ export function namesPullRequest(subject: string): boolean {
48
+ return PULL_REQUEST.test(subject);
49
+ }
50
+
45
51
  function entryOf(subject: string, repositoryUrl: string): Entry | undefined {
46
52
  const group = groupOf(subject);
47
53
  if (group === undefined) return undefined;
48
54
  const [, , scope = "", , description = ""] = CONVENTIONAL.exec(subject) ?? [];
49
- const linkedDescription = description.replace(/ \(#(\d+)\)$/, (_, number: string) => ` [#${number}](${repositoryUrl}/pull/${number})`);
55
+ const linkedDescription = description.replace(PULL_REQUEST, (_, number: string) => ` [#${number}](${repositoryUrl}/pull/${number})`);
50
56
  return { group, text: scope === "" ? linkedDescription : `**${scope}:** ${linkedDescription}` };
51
57
  }
52
58
 
@@ -82,6 +88,10 @@ export function cuts(
82
88
  }));
83
89
  }
84
90
 
91
+ export function hasEntries(subjects: readonly string[]): boolean {
92
+ return subjects.some((subject) => groupOf(subject) !== undefined);
93
+ }
94
+
85
95
  function renderRelease({ version, date, subjects }: Release, repositoryUrl: string): readonly string[] {
86
96
  const entries = subjects.flatMap((subject) => entryOf(subject, repositoryUrl) ?? []);
87
97
  const groups = CHANGE_GROUPS.flatMap((group) => {
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env bun
2
2
  import { Console, Effect, FileSystem, Path, Schema } from "effect";
3
- import { defaultBranch, git } from "./git.ts";
4
- import { runMain } from "./main.ts";
5
- import { ENTRY_POINT, KIT_GATES, type KitGate } from "./gates.ts";
3
+ import { defaultBranch, git } from "../core/git.ts";
4
+ import { runMain } from "../core/main.ts";
5
+ import { ENTRY_POINT, KIT_GATES, type KitGate } from "../core/gates.ts";
6
6
  import { invokes, mentions, plainCommand, type Command } from "./shell-command.ts";
7
7
 
8
8
  export type { Command };
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
  import { Console, Effect, FileSystem, Path, Schema } from "effect";
3
- import { git, refArgs } from "./git.ts";
4
- import { runMain } from "./main.ts";
3
+ import { git, refArgs } from "../core/git.ts";
4
+ import { runMain } from "../core/main.ts";
5
5
 
6
6
  type Identity = { readonly name: string; readonly email: string };
7
7
 
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  import { Effect, FileSystem, Schema } from "effect";
3
- import { runMain, Usage } from "./main.ts";
3
+ import { runMain, Usage } from "../core/main.ts";
4
4
 
5
5
  export class ReleaseSectionUnavailable extends Schema.TaggedError<ReleaseSectionUnavailable>()("ReleaseSectionUnavailable", {
6
6
  message: Schema.String,
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env bun
2
2
  import { Console, Effect, Schema } from "effect";
3
3
  import { groupOf } from "./changelog.ts";
4
- import { git } from "./git.ts";
5
- import { runMain, Usage } from "./main.ts";
4
+ import { git } from "../core/git.ts";
5
+ import { runMain, Usage } from "../core/main.ts";
6
6
 
7
7
  export class ReleaseReportUnreadable extends Schema.TaggedError<ReleaseReportUnreadable>()("ReleaseReportUnreadable", {
8
8
  message: Schema.String,
@@ -1,5 +1,5 @@
1
1
  import { Effect, Schema } from "effect";
2
- import { Usage } from "./main.ts";
2
+ import { Usage } from "../core/main.ts";
3
3
 
4
4
  export const NAME = "checks-vendor";
5
5
  export const OPENER = "--library";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
  import { Console, Effect, FileSystem, Option, Path, Schema } from "effect";
3
- import { git } from "./git.ts";
4
- import { runMain } from "./main.ts";
3
+ import { git } from "../core/git.ts";
4
+ import { runMain } from "../core/main.ts";
5
5
  import { librariesFrom, NAME, OPENER, type Library } from "./vendor-args.ts";
6
6
 
7
7
  const CACHE_HOME = ".cache/avi2dg-checks";
@@ -1,6 +1,6 @@
1
1
  import { Effect, FileSystem, Path, Schema } from "effect";
2
2
  import { anchoredTargets, anchorsOf, PACKAGE_MANIFEST, packagesOf, snapshotOf, type Unresolved } from "./doc-references.ts";
3
- import { git, pathsAt } from "./git.ts";
3
+ import { git, pathsAt } from "../core/git.ts";
4
4
 
5
5
  export class ManifestUnreadable extends Schema.TaggedError<ManifestUnreadable>()("ManifestUnreadable", {
6
6
  message: Schema.String,
@@ -91,7 +91,7 @@ export const TEMPLATES: Readonly<Record<Kind, Template>> = {
91
91
  title: { type: "fixed", text: "Changelog" },
92
92
  lead: ["Every release of <package>, newest first, written by the release from its conventional commits."],
93
93
  sections: [
94
- open("<version>", "version", REQUIRED, ["Released <YYYY-MM-DD>."], {
94
+ open("<version>", "version", optional("no release holds a conventional commit worth listing yet"), ["Released <YYYY-MM-DD>."], {
95
95
  subsections: CHANGE_GROUPS.map((group) =>
96
96
  fixed(group, optional("the release holds no such commit"), ["- **<the commit's scope, when it has one>:** <its description>"]),
97
97
  ),
@@ -3,8 +3,8 @@ import { Console, Effect } from "effect";
3
3
  import { rootsOf, unresolvedIn, type Judging, type Unresolved } from "./doc-references.ts";
4
4
  import { ADR_DIRECTORY, judge, placementOf, placementProblem, speaksToConsumers, type Placement } from "./doc-rules.ts";
5
5
  import { readTexts, snapshotAt, stillMissing } from "./doc-snapshot.ts";
6
- import { changedLines, changedPaths, git, pathsAt, rangeEnds, refArgs } from "./git.ts";
7
- import { runMain } from "./main.ts";
6
+ import { changedLines, changedPaths, git, pathsAt, rangeEnds, refArgs } from "../core/git.ts";
7
+ import { runMain } from "../core/main.ts";
8
8
  import { isLivingDoc, proseFindings, readerOf } from "./prose-matchers.ts";
9
9
 
10
10
  type Finding = {
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env bun
2
2
  import { Console, Effect, Option } from "effect";
3
3
  import { refused, syntaxOf } from "./comments.ts";
4
- import { changedLines, git, parentOrEmptyTree, refArgs } from "./git.ts";
5
- import { runMain } from "./main.ts";
4
+ import { changedLines, git, parentOrEmptyTree, refArgs } from "../core/git.ts";
5
+ import { runMain } from "../core/main.ts";
6
6
 
7
7
  export type GateResult = {
8
8
  readonly files: number;
@@ -88,7 +88,7 @@ export function syntaxOf(path: string): Syntax | undefined {
88
88
  }
89
89
 
90
90
  export function unreadable(path: string): string {
91
- return `${path} is code the comment checks cannot read: add a comment syntax for .${extensionOf(path)} to scripts/comment-matchers.ts`;
91
+ return `${path} is code the comment checks cannot read: add a comment syntax for .${extensionOf(path)} to src/quality/comment-matchers.ts`;
92
92
  }
93
93
 
94
94
  const WORD = /[\w$]/;
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
  import { Config, Console, Effect, FileSystem, Option, Path, Random, Schema } from "effect";
3
3
  import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
4
- import { runMain, Usage } from "./main.ts";
4
+ import { runMain, Usage } from "../core/main.ts";
5
5
  import { NAME_SEPARATOR, parseReport, ReportError, reporterArgs, type TestResult } from "./test-report.ts";
6
6
 
7
7
  export type Run = {