@avi2dg/checks 0.22.0 → 0.24.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/CHANGELOG.md +20 -0
- package/CONTRIBUTING.md +6 -5
- package/README.md +4 -1
- package/dist/effect-channel/index.js +122 -0
- package/dist/{index.js → readability/index.js} +8 -120
- package/docs/configs/native-settings.md +2 -2
- package/docs/design.md +7 -2
- package/docs/gates/checks-changelog.md +57 -0
- package/docs/gates/checks-release-notes.md +141 -0
- package/docs/gates/checks-release-report.md +64 -0
- package/docs/gates/checks-repetition.md +1 -0
- package/docs/gates/checks-vendor.md +11 -2
- package/oxlintrc.json +1 -1
- package/package.json +18 -4
- package/scripts/changelog-write.ts +95 -0
- package/scripts/changelog.ts +110 -0
- package/scripts/release-notes.ts +37 -0
- package/scripts/release-report.ts +50 -0
- package/scripts/repetition.ts +14 -3
- package/scripts/vendor-args.ts +54 -0
- package/scripts/vendor.ts +36 -66
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
Every release of `@avi2dg/checks`, newest first, written by the release from its conventional commits.
|
|
4
4
|
|
|
5
|
+
## 0.24.0
|
|
6
|
+
|
|
7
|
+
Released 2026-09-27.
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- **scripts:** ship checks-changelog, checks-release-notes and checks-release-report bins [#77](https://github.com/avi2d/checks/pull/77)
|
|
12
|
+
|
|
13
|
+
### Fixes
|
|
14
|
+
|
|
15
|
+
- **scripts:** make checks-vendor re-freeze a cached tree whose owner write bit came back [#76](https://github.com/avi2d/checks/pull/76)
|
|
16
|
+
|
|
17
|
+
## 0.23.0
|
|
18
|
+
|
|
19
|
+
Released 2026-09-26.
|
|
20
|
+
|
|
21
|
+
### Breaking changes
|
|
22
|
+
|
|
23
|
+
- move cognitive-complexity into its own readability oxlint plugin [#74](https://github.com/avi2d/checks/pull/74)
|
|
24
|
+
|
|
5
25
|
## 0.22.0
|
|
6
26
|
|
|
7
27
|
Released 2026-09-26.
|
package/CONTRIBUTING.md
CHANGED
|
@@ -26,8 +26,8 @@ Each generated file is committed, and lint, the suite or CI's diff after the bui
|
|
|
26
26
|
|
|
27
27
|
To regenerate after an edit:
|
|
28
28
|
|
|
29
|
-
1. After editing `effect-channel/` or the templates in `scripts/doc-templates.ts`, run `bun run build`.
|
|
30
|
-
It rewrites `dist
|
|
29
|
+
1. After editing `effect-channel/`, `readability/` or the templates in `scripts/doc-templates.ts`, run `bun run build`.
|
|
30
|
+
It rewrites `dist/` and `templates/`.
|
|
31
31
|
1. After editing anything a generated block names as its source in its opening marker, run `bun run build`, which rewrites every generated block.
|
|
32
32
|
1. Commit what the command rewrote in the same commit as the edit.
|
|
33
33
|
|
|
@@ -68,8 +68,9 @@ To place a change:
|
|
|
68
68
|
| Path | What it holds |
|
|
69
69
|
| --- | --- |
|
|
70
70
|
| `scripts/` | every bin, and the modules they share |
|
|
71
|
-
| `effect-channel/` | the oxlint plugin with the Effect error-channel
|
|
72
|
-
| `
|
|
71
|
+
| `effect-channel/` | the oxlint plugin with the Effect error-channel rules |
|
|
72
|
+
| `readability/` | the oxlint plugin with the readability rules |
|
|
73
|
+
| `dist/` | the committed oxlint plugin bundles |
|
|
73
74
|
| `presets/` | the Effect rule blocks consumers copy into native configs |
|
|
74
75
|
| `templates/` | one template per kind of doc file, which `bun run build` renders |
|
|
75
76
|
| `CHANGELOG.md` | every release, which `bun run build` writes from the conventional commits |
|
|
@@ -83,7 +84,7 @@ To place a change:
|
|
|
83
84
|
A new bin gets its page under `docs/gates/`, and the suite fails until it has one.
|
|
84
85
|
|
|
85
86
|
This repository holds itself to the kit, with two exceptions of its own.
|
|
86
|
-
Its `.dependency-cruiser.cjs` redeclares `no-orphans` with
|
|
87
|
+
Its `.dependency-cruiser.cjs` redeclares `no-orphans` with each plugin entry added to its `pathNot`.
|
|
87
88
|
Its `.oxlintrc.json` lifts `effect-channel/no-throw` from `scripts/comment-matchers.ts`, whose synchronous `refused()` a host loads without `node_modules`.
|
|
88
89
|
|
|
89
90
|
## Related topics
|
package/README.md
CHANGED
|
@@ -126,6 +126,9 @@ These bins run on their own:
|
|
|
126
126
|
- [`checks-mutation-compare`](docs/gates/checks-mutation-compare.md) holds every mutant in a pull request to no regression.
|
|
127
127
|
- [`checks-subsumed-tests`](docs/gates/checks-subsumed-tests.md) lists each test another test subsumes in a mutation run.
|
|
128
128
|
- [`checks-backtest`](docs/gates/checks-backtest.md) reports what the comment check would have refused in recent history.
|
|
129
|
+
- [`checks-changelog`](docs/gates/checks-changelog.md) writes the pending release into `CHANGELOG.md` from the conventional commits since the last release.
|
|
130
|
+
- [`checks-release-notes`](docs/gates/checks-release-notes.md) writes one `CHANGELOG.md` section to a file for a GitHub release.
|
|
131
|
+
- [`checks-release-report`](docs/gates/checks-release-report.md) tells whether the history holds unreleased features or fixes since the last tag.
|
|
129
132
|
- [`checks-vendor`](docs/gates/checks-vendor.md) pins each library its `prepare` arguments name to a shared read-only clone and links it under `repos/`.
|
|
130
133
|
|
|
131
134
|
`checks-lint` has [its own page](docs/gates/checks-lint.md), which says which range it resolves.
|
|
@@ -164,7 +167,7 @@ Every path is relative to the installed package, `node_modules/@avi2dg/checks/`.
|
|
|
164
167
|
| `oxlintrc.json` | the oxlint base config `.oxlintrc.json` extends |
|
|
165
168
|
| `stryker.preset.js` | the Stryker mutation-testing preset |
|
|
166
169
|
| `tsconfig.effect.json` | the tsconfig fragment with the Effect language-service block |
|
|
167
|
-
| `dist/` | the compiled oxlint
|
|
170
|
+
| `dist/` | the compiled oxlint plugins, one per purpose |
|
|
168
171
|
|
|
169
172
|
<!-- end generated shipped -->
|
|
170
173
|
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// effect-channel/no-error-channel-escape.ts
|
|
2
|
+
var EFFECT_SOURCES = new Set(["effect", "effect/Effect"]);
|
|
3
|
+
var INSTEAD = {
|
|
4
|
+
drops: "it drops the failure and the success together, so no caller can tell one from the other. Handle the error by tag, or keep it as a value with Effect.result or Effect.exit",
|
|
5
|
+
swallows: "a Cause is the typed error plus defects plus interruption, so this swallows the bugs and the cancellations along with it. Catch the errors you named, with Effect.catchTag or Effect.catchTags",
|
|
6
|
+
blind: "the handler cannot see what it is recovering from, so every error in the channel collapses into one fallback. Name the errors with Effect.catchTag, or take the error and use it"
|
|
7
|
+
};
|
|
8
|
+
var REFUSED = new Map([
|
|
9
|
+
["ignore", INSTEAD.drops],
|
|
10
|
+
["ignoreCause", INSTEAD.drops],
|
|
11
|
+
["catchCause", INSTEAD.swallows],
|
|
12
|
+
["catchCauseIf", INSTEAD.swallows],
|
|
13
|
+
["catchCauseFilter", INSTEAD.swallows]
|
|
14
|
+
]);
|
|
15
|
+
var blindToTheError = (handler) => {
|
|
16
|
+
if (!handler)
|
|
17
|
+
return false;
|
|
18
|
+
if (handler.type !== "ArrowFunctionExpression" && handler.type !== "FunctionExpression")
|
|
19
|
+
return false;
|
|
20
|
+
return handler.params.every((param) => param.type === "Identifier" && /^_+$/.test(param.name));
|
|
21
|
+
};
|
|
22
|
+
var rule = {
|
|
23
|
+
meta: {
|
|
24
|
+
type: "problem",
|
|
25
|
+
docs: { description: "Disallow the combinators that erase Effect's error channel" }
|
|
26
|
+
},
|
|
27
|
+
create(context) {
|
|
28
|
+
const effect = new Set;
|
|
29
|
+
const named = (node) => {
|
|
30
|
+
if (!node || node.type !== "MemberExpression" || node.computed)
|
|
31
|
+
return null;
|
|
32
|
+
if (node.object.type !== "Identifier" || !effect.has(node.object.name))
|
|
33
|
+
return null;
|
|
34
|
+
return node.property.type === "Identifier" ? node.property.name : null;
|
|
35
|
+
};
|
|
36
|
+
const refuse = (node, combinator, instead) => {
|
|
37
|
+
context.report({ node, message: `Effect.${combinator} erases the error channel: ${instead}` });
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
ImportDeclaration(node) {
|
|
41
|
+
if (!EFFECT_SOURCES.has(node.source.value))
|
|
42
|
+
return;
|
|
43
|
+
const module = node.source.value === "effect/Effect";
|
|
44
|
+
for (const specifier of node.specifiers) {
|
|
45
|
+
if (specifier.type === "ImportSpecifier") {
|
|
46
|
+
if (specifier.imported.type === "Identifier" && specifier.imported.name === "Effect")
|
|
47
|
+
effect.add(specifier.local.name);
|
|
48
|
+
} else if (module)
|
|
49
|
+
effect.add(specifier.local.name);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
MemberExpression(node) {
|
|
53
|
+
const combinator = named(node);
|
|
54
|
+
if (combinator === null)
|
|
55
|
+
return;
|
|
56
|
+
const instead = REFUSED.get(combinator);
|
|
57
|
+
if (instead !== undefined)
|
|
58
|
+
refuse(node, combinator, instead);
|
|
59
|
+
},
|
|
60
|
+
CallExpression(node) {
|
|
61
|
+
if (named(node.callee) !== "catch")
|
|
62
|
+
return;
|
|
63
|
+
if (!blindToTheError(node.arguments[node.arguments.length - 1]))
|
|
64
|
+
return;
|
|
65
|
+
refuse(node, "catch", INSTEAD.blind);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
var no_error_channel_escape_default = rule;
|
|
71
|
+
|
|
72
|
+
// effect-channel/no-throw.ts
|
|
73
|
+
var rule2 = {
|
|
74
|
+
meta: {
|
|
75
|
+
type: "problem",
|
|
76
|
+
docs: { description: "Disallow throw, which fails outside Effect's error channel" }
|
|
77
|
+
},
|
|
78
|
+
create(context) {
|
|
79
|
+
return {
|
|
80
|
+
ThrowStatement(node) {
|
|
81
|
+
context.report({
|
|
82
|
+
node,
|
|
83
|
+
message: "throw escapes the error channel: no type records the failure, so no caller has to answer for it. Define the failure with Schema.TaggedError and fail with it through Effect.fail, so it stays in E for Effect.catchTag to handle"
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
var no_throw_default = rule2;
|
|
90
|
+
|
|
91
|
+
// effect-channel/no-try-catch.ts
|
|
92
|
+
var rule3 = {
|
|
93
|
+
meta: {
|
|
94
|
+
type: "problem",
|
|
95
|
+
docs: { description: "Disallow a try statement with a catch clause, which recovers outside Effect's error channel" }
|
|
96
|
+
},
|
|
97
|
+
create(context) {
|
|
98
|
+
return {
|
|
99
|
+
CatchClause(node) {
|
|
100
|
+
context.report({
|
|
101
|
+
node,
|
|
102
|
+
message: "catch recovers outside the error channel: it takes whatever was thrown as unknown, bugs included. Wrap the throwing call in Effect.try or Effect.tryPromise, whose catch maps the cause to a Schema.TaggedError, and recover by tag with Effect.catchTag"
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
var no_try_catch_default = rule3;
|
|
109
|
+
|
|
110
|
+
// effect-channel/index.ts
|
|
111
|
+
var plugin = {
|
|
112
|
+
meta: { name: "effect-channel" },
|
|
113
|
+
rules: {
|
|
114
|
+
"no-error-channel-escape": no_error_channel_escape_default,
|
|
115
|
+
"no-throw": no_throw_default,
|
|
116
|
+
"no-try-catch": no_try_catch_default
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
var effect_channel_default = plugin;
|
|
120
|
+
export {
|
|
121
|
+
effect_channel_default as default
|
|
122
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// readability/cognitive-nodes.ts
|
|
2
2
|
var CONTROL_TYPES = ["IfStatement", "ConditionalExpression", "SwitchStatement", "SwitchCase", "TryStatement", "CatchClause"];
|
|
3
3
|
var LOOP_TYPES = ["ForStatement", "ForInStatement", "ForOfStatement", "WhileStatement", "DoWhileStatement", "LabeledStatement"];
|
|
4
4
|
var CALL_TYPES = ["LogicalExpression", "BreakStatement", "ContinueStatement", "CallExpression", "NewExpression", "ImportExpression"];
|
|
@@ -7,7 +7,7 @@ var PLAIN_A_TYPES = ["BlockStatement", "ExpressionStatement", "ReturnStatement",
|
|
|
7
7
|
var PLAIN_B_TYPES = ["ClassDeclaration", "ClassExpression", "ClassBody", "MethodDefinition", "TSAbstractMethodDefinition", "PropertyDefinition", "TSAbstractPropertyDefinition", "AccessorProperty", "TSAbstractAccessorProperty", "ObjectExpression", "ArrayExpression"];
|
|
8
8
|
var PLAIN_C_TYPES = ["AwaitExpression", "UnaryExpression", "UpdateExpression", "SpreadElement", "YieldExpression", "BinaryExpression", "AssignmentExpression", "TSAsExpression", "TSSatisfiesExpression", "TSTypeAssertion", "TSNonNullExpression", "ChainExpression", "ParenthesizedExpression", "Decorator", "TSInstantiationExpression", "MemberExpression", "JSXMemberExpression", "TemplateLiteral", "TaggedTemplateExpression", "SequenceExpression", "JSXElement", "JSXFragment", "JSXOpeningElement", "JSXAttribute", "JSXExpressionContainer", "JSXSpreadChild", "JSXSpreadAttribute"];
|
|
9
9
|
|
|
10
|
-
//
|
|
10
|
+
// readability/cognitive-plain.ts
|
|
11
11
|
function isControl(node) {
|
|
12
12
|
return CONTROL_TYPES.includes(node.type);
|
|
13
13
|
}
|
|
@@ -142,7 +142,7 @@ function plainChildrenC(node) {
|
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
//
|
|
145
|
+
// readability/cognitive.ts
|
|
146
146
|
function unreachable2(_value) {}
|
|
147
147
|
function scoreList(state, nodes, nesting, parent) {
|
|
148
148
|
for (const node of nodes) {
|
|
@@ -334,7 +334,7 @@ function cognitiveComplexity(root, names) {
|
|
|
334
334
|
return state.recursive ? state.total + 1 : state.total;
|
|
335
335
|
}
|
|
336
336
|
|
|
337
|
-
//
|
|
337
|
+
// readability/cognitive-complexity.ts
|
|
338
338
|
var DEFAULT_MAX = 15;
|
|
339
339
|
function maxOf(options) {
|
|
340
340
|
const [first] = options;
|
|
@@ -416,126 +416,14 @@ var rule = {
|
|
|
416
416
|
};
|
|
417
417
|
var cognitive_complexity_default = rule;
|
|
418
418
|
|
|
419
|
-
//
|
|
420
|
-
var EFFECT_SOURCES = new Set(["effect", "effect/Effect"]);
|
|
421
|
-
var INSTEAD = {
|
|
422
|
-
drops: "it drops the failure and the success together, so no caller can tell one from the other. Handle the error by tag, or keep it as a value with Effect.result or Effect.exit",
|
|
423
|
-
swallows: "a Cause is the typed error plus defects plus interruption, so this swallows the bugs and the cancellations along with it. Catch the errors you named, with Effect.catchTag or Effect.catchTags",
|
|
424
|
-
blind: "the handler cannot see what it is recovering from, so every error in the channel collapses into one fallback. Name the errors with Effect.catchTag, or take the error and use it"
|
|
425
|
-
};
|
|
426
|
-
var REFUSED = new Map([
|
|
427
|
-
["ignore", INSTEAD.drops],
|
|
428
|
-
["ignoreCause", INSTEAD.drops],
|
|
429
|
-
["catchCause", INSTEAD.swallows],
|
|
430
|
-
["catchCauseIf", INSTEAD.swallows],
|
|
431
|
-
["catchCauseFilter", INSTEAD.swallows]
|
|
432
|
-
]);
|
|
433
|
-
var blindToTheError = (handler) => {
|
|
434
|
-
if (!handler)
|
|
435
|
-
return false;
|
|
436
|
-
if (handler.type !== "ArrowFunctionExpression" && handler.type !== "FunctionExpression")
|
|
437
|
-
return false;
|
|
438
|
-
return handler.params.every((param) => param.type === "Identifier" && /^_+$/.test(param.name));
|
|
439
|
-
};
|
|
440
|
-
var rule2 = {
|
|
441
|
-
meta: {
|
|
442
|
-
type: "problem",
|
|
443
|
-
docs: { description: "Disallow the combinators that erase Effect's error channel" }
|
|
444
|
-
},
|
|
445
|
-
create(context) {
|
|
446
|
-
const effect = new Set;
|
|
447
|
-
const named = (node) => {
|
|
448
|
-
if (!node || node.type !== "MemberExpression" || node.computed)
|
|
449
|
-
return null;
|
|
450
|
-
if (node.object.type !== "Identifier" || !effect.has(node.object.name))
|
|
451
|
-
return null;
|
|
452
|
-
return node.property.type === "Identifier" ? node.property.name : null;
|
|
453
|
-
};
|
|
454
|
-
const refuse = (node, combinator, instead) => {
|
|
455
|
-
context.report({ node, message: `Effect.${combinator} erases the error channel: ${instead}` });
|
|
456
|
-
};
|
|
457
|
-
return {
|
|
458
|
-
ImportDeclaration(node) {
|
|
459
|
-
if (!EFFECT_SOURCES.has(node.source.value))
|
|
460
|
-
return;
|
|
461
|
-
const module = node.source.value === "effect/Effect";
|
|
462
|
-
for (const specifier of node.specifiers) {
|
|
463
|
-
if (specifier.type === "ImportSpecifier") {
|
|
464
|
-
if (specifier.imported.type === "Identifier" && specifier.imported.name === "Effect")
|
|
465
|
-
effect.add(specifier.local.name);
|
|
466
|
-
} else if (module)
|
|
467
|
-
effect.add(specifier.local.name);
|
|
468
|
-
}
|
|
469
|
-
},
|
|
470
|
-
MemberExpression(node) {
|
|
471
|
-
const combinator = named(node);
|
|
472
|
-
if (combinator === null)
|
|
473
|
-
return;
|
|
474
|
-
const instead = REFUSED.get(combinator);
|
|
475
|
-
if (instead !== undefined)
|
|
476
|
-
refuse(node, combinator, instead);
|
|
477
|
-
},
|
|
478
|
-
CallExpression(node) {
|
|
479
|
-
if (named(node.callee) !== "catch")
|
|
480
|
-
return;
|
|
481
|
-
if (!blindToTheError(node.arguments[node.arguments.length - 1]))
|
|
482
|
-
return;
|
|
483
|
-
refuse(node, "catch", INSTEAD.blind);
|
|
484
|
-
}
|
|
485
|
-
};
|
|
486
|
-
}
|
|
487
|
-
};
|
|
488
|
-
var no_error_channel_escape_default = rule2;
|
|
489
|
-
|
|
490
|
-
// effect-channel/no-throw.ts
|
|
491
|
-
var rule3 = {
|
|
492
|
-
meta: {
|
|
493
|
-
type: "problem",
|
|
494
|
-
docs: { description: "Disallow throw, which fails outside Effect's error channel" }
|
|
495
|
-
},
|
|
496
|
-
create(context) {
|
|
497
|
-
return {
|
|
498
|
-
ThrowStatement(node) {
|
|
499
|
-
context.report({
|
|
500
|
-
node,
|
|
501
|
-
message: "throw escapes the error channel: no type records the failure, so no caller has to answer for it. Define the failure with Schema.TaggedError and fail with it through Effect.fail, so it stays in E for Effect.catchTag to handle"
|
|
502
|
-
});
|
|
503
|
-
}
|
|
504
|
-
};
|
|
505
|
-
}
|
|
506
|
-
};
|
|
507
|
-
var no_throw_default = rule3;
|
|
508
|
-
|
|
509
|
-
// effect-channel/no-try-catch.ts
|
|
510
|
-
var rule4 = {
|
|
511
|
-
meta: {
|
|
512
|
-
type: "problem",
|
|
513
|
-
docs: { description: "Disallow a try statement with a catch clause, which recovers outside Effect's error channel" }
|
|
514
|
-
},
|
|
515
|
-
create(context) {
|
|
516
|
-
return {
|
|
517
|
-
CatchClause(node) {
|
|
518
|
-
context.report({
|
|
519
|
-
node,
|
|
520
|
-
message: "catch recovers outside the error channel: it takes whatever was thrown as unknown, bugs included. Wrap the throwing call in Effect.try or Effect.tryPromise, whose catch maps the cause to a Schema.TaggedError, and recover by tag with Effect.catchTag"
|
|
521
|
-
});
|
|
522
|
-
}
|
|
523
|
-
};
|
|
524
|
-
}
|
|
525
|
-
};
|
|
526
|
-
var no_try_catch_default = rule4;
|
|
527
|
-
|
|
528
|
-
// effect-channel/index.ts
|
|
419
|
+
// readability/index.ts
|
|
529
420
|
var plugin = {
|
|
530
|
-
meta: { name: "
|
|
421
|
+
meta: { name: "readability" },
|
|
531
422
|
rules: {
|
|
532
|
-
"no-error-channel-escape": no_error_channel_escape_default,
|
|
533
|
-
"no-throw": no_throw_default,
|
|
534
|
-
"no-try-catch": no_try_catch_default,
|
|
535
423
|
"cognitive-complexity": cognitive_complexity_default
|
|
536
424
|
}
|
|
537
425
|
};
|
|
538
|
-
var
|
|
426
|
+
var readability_default = plugin;
|
|
539
427
|
export {
|
|
540
|
-
|
|
428
|
+
readability_default as default
|
|
541
429
|
};
|
|
@@ -57,12 +57,12 @@ The kit's own `.oxlintrc.json` sets these limits for each size override, and a r
|
|
|
57
57
|
|
|
58
58
|
<!-- generated size-limits: bun run build writes it from .oxlintrc.json, SIZE_RULES in scripts/size-rules.ts and scripts/doc-blocks.ts -->
|
|
59
59
|
|
|
60
|
-
| Limits | oxlint rule | `effect-channel/**/*.ts`, `scripts/**/*.ts` | `tests/**/*.ts` |
|
|
60
|
+
| Limits | oxlint rule | `effect-channel/**/*.ts`, `readability/**/*.ts`, `scripts/**/*.ts` | `tests/**/*.ts` |
|
|
61
61
|
| --- | --- | --- | --- |
|
|
62
62
|
| The most lines a file may hold, blank and comment lines counted | `max-lines` | 400 | 600 |
|
|
63
63
|
| The most lines a function may span, blank and comment lines counted | `max-lines-per-function` | 100 | off |
|
|
64
64
|
| The most statements a function may hold | `max-statements` | 30 | 50 |
|
|
65
|
-
| The highest cognitive complexity a function may reach, a switch counted once | `
|
|
65
|
+
| The highest cognitive complexity a function may reach, a switch counted once | `readability/cognitive-complexity` | 15 | 15 |
|
|
66
66
|
| The deepest a block may nest inside a function | `max-depth` | 4 | 4 |
|
|
67
67
|
|
|
68
68
|
<!-- end generated size-limits -->
|
package/docs/design.md
CHANGED
|
@@ -12,7 +12,7 @@ Each entry below is a choice in the kit's shape and the constraint that forced i
|
|
|
12
12
|
- `files` in package.json is the published surface: `tests/`, `AGENTS.md` and the `.ts` plugin source never reach an install.
|
|
13
13
|
npm adds `package.json`, `README` and `LICENSE` to the tarball whatever `files` says.
|
|
14
14
|
`bun pm pack` builds the same tarball the registry serves, which is what the packed-tarball consumer e2e test installs.
|
|
15
|
-
-
|
|
15
|
+
- Each plugin ships compiled under `dist/`, built with `bun build <name>/index.ts --outdir dist/<name> --target node --format esm`.
|
|
16
16
|
Node refuses to type-strip a `.ts` plugin under `node_modules`, so the `.ts` source would fail to load from an installed package.
|
|
17
17
|
- The size rules are plain oxlint rules at `error` in `.oxlintrc.json`, and `bun run lint` enforces them on the whole tree.
|
|
18
18
|
A repository records its existing violations with `oxlint --suppress-all`, and `checks-suppressions-ratchet` refuses any count that rises.
|
|
@@ -51,7 +51,7 @@ Each entry below is a choice in the kit's shape and the constraint that forced i
|
|
|
51
51
|
`@effect/platform-node-shared` is a direct dependency at the same exact version only to pin it: `@effect/platform-bun` asks for it with a `^` range, and a newer rc peers on a newer `effect` than consumers install, so all three move together.
|
|
52
52
|
- Each runnable script ships a `checks-` bin entry, so consumer `package.json` scripts call the short name, which the package manager puts on `PATH` only there.
|
|
53
53
|
A shell runs it through `bun run`, which never falls back to the registry the way `bunx` does.
|
|
54
|
-
The `.ts` checks keep a `bun` shebang, which needs no build step and no `dist/` entry, unlike the oxlint
|
|
54
|
+
The `.ts` checks keep a `bun` shebang, which needs no build step and no `dist/` entry, unlike the oxlint plugins that node loads.
|
|
55
55
|
- `checks-lint` runs each gate as its own bin in a child process rather than importing it, so a gate behaves the same called alone or through the entry point, and `lint-coverage.sh` stays a shell script.
|
|
56
56
|
The gates run one at a time with their output passed straight through, so each report reads whole and in the table's order.
|
|
57
57
|
- `checks-lint` determines applicable gates from tracked files instead of accepting a repository selection.
|
|
@@ -96,6 +96,11 @@ Each entry below is a choice in the kit's shape and the constraint that forced i
|
|
|
96
96
|
A directory the range deletes still counts as this repository's, so a path under it reads as stale rather than foreign.
|
|
97
97
|
- The command check passes over a page whose front matter sets `audience: consumers`.
|
|
98
98
|
Such a page speaks to a consuming repository, whose scripts are not this one's.
|
|
99
|
+
- `checks-vendor` strips an owner write bit that came back on a cached tree and keeps the tree, rather than refusing it or cloning it again.
|
|
100
|
+
The GitHub Actions runner clears the read only mode of each item before it deletes `$RUNNER_TEMP`, and on a directory link that chmod lands on the shared tree's top directory.
|
|
101
|
+
Refusing the tree would fail every later job on the runner until a person cleared it, and cloning it again would need the network after every such job.
|
|
102
|
+
A write bit is not a write, so the run strips it and then holds the tree to the recorded commit as it holds any tree, and a write it finds there still fails the run.
|
|
103
|
+
A group or other write bit still fails the run, since another user could have edited `.git/config` through it before `git status` reads it.
|
|
99
104
|
|
|
100
105
|
## Related topics
|
|
101
106
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: reference
|
|
3
|
+
audience: consumers
|
|
4
|
+
---
|
|
5
|
+
# checks-changelog
|
|
6
|
+
|
|
7
|
+
`checks-changelog` writes the pending release into `CHANGELOG.md` from the conventional commits since the last release.
|
|
8
|
+
|
|
9
|
+
## What it checks
|
|
10
|
+
|
|
11
|
+
It reads the version from `package.json` and treats that version as the release being prepared.
|
|
12
|
+
It lists every conventional commit the release closes, grouped as Features, Fixes, Performance, Reverts and Breaking changes.
|
|
13
|
+
It links each entry to its pull request under the repository address `package.json` names.
|
|
14
|
+
It keeps the date a released section already carries and dates a new section today.
|
|
15
|
+
It writes the whole file newest first, so the changelog is never edited by hand.
|
|
16
|
+
A repository with no tag yet releases from its first commit.
|
|
17
|
+
A release with no conventional commit worth listing keeps only its heading and its date.
|
|
18
|
+
|
|
19
|
+
## What it reads
|
|
20
|
+
|
|
21
|
+
It reads `package.json`, `CHANGELOG.md` and the git history from the repository root.
|
|
22
|
+
It finds releases in the version bumps of `package.json` across all of `HEAD` ancestry, so a checkout without tags writes the same file.
|
|
23
|
+
It refuses a shallow checkout, since the releases reach back past its history.
|
|
24
|
+
It refuses a `package.json` with no repository address, since each entry links its pull request under it.
|
|
25
|
+
It refuses a repository address that is no `https` address once `git+`, a trailing slash and `.git` are dropped, since a pull request link needs one.
|
|
26
|
+
|
|
27
|
+
## Arguments
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
checks-changelog
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
It takes no arguments.
|
|
34
|
+
Run it through the build, as the release workflow in [checks-release-notes](checks-release-notes.md) shows.
|
|
35
|
+
|
|
36
|
+
## Exit codes
|
|
37
|
+
|
|
38
|
+
| Code | Result |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| 0 | The changelog was written. |
|
|
41
|
+
| 2 | The checkout is shallow, or `package.json` has no `https` repository address. |
|
|
42
|
+
|
|
43
|
+
## Sample output
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
checks-changelog: wrote 2 release(s) to CHANGELOG.md
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Opting out
|
|
50
|
+
|
|
51
|
+
Nothing runs it but the build of a repository that keeps a changelog.
|
|
52
|
+
A repository with no versioned releases leaves it out.
|
|
53
|
+
|
|
54
|
+
## Related topics
|
|
55
|
+
|
|
56
|
+
- [checks-release-notes](checks-release-notes.md)
|
|
57
|
+
- [checks-release-report](checks-release-report.md)
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: reference
|
|
3
|
+
audience: consumers
|
|
4
|
+
---
|
|
5
|
+
# checks-release-notes
|
|
6
|
+
|
|
7
|
+
`checks-release-notes` writes the `CHANGELOG.md` section of one version to a file for a GitHub release.
|
|
8
|
+
|
|
9
|
+
## What it checks
|
|
10
|
+
|
|
11
|
+
It reads the section the version heads and trims the blank lines around it.
|
|
12
|
+
It writes the notes to the output path.
|
|
13
|
+
It fails when the changelog holds no section for the version or the section is empty.
|
|
14
|
+
|
|
15
|
+
## What it reads
|
|
16
|
+
|
|
17
|
+
It reads `CHANGELOG.md` from the working directory.
|
|
18
|
+
It reads the version from its arguments, not from `package.json`.
|
|
19
|
+
|
|
20
|
+
## Arguments
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
checks-release-notes <version> <output>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The version names the `CHANGELOG.md` section to write, as `0.22.0` with no leading `v`.
|
|
27
|
+
The output names the file to write.
|
|
28
|
+
|
|
29
|
+
## Exit codes
|
|
30
|
+
|
|
31
|
+
| Code | Result |
|
|
32
|
+
| --- | --- |
|
|
33
|
+
| 0 | The notes were written. |
|
|
34
|
+
| 2 | The arguments do not parse, or `CHANGELOG.md` holds no notes for the version. |
|
|
35
|
+
|
|
36
|
+
## Sample output
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
Released 2026-09-27.
|
|
40
|
+
|
|
41
|
+
### Features
|
|
42
|
+
|
|
43
|
+
- Add a release [#67](https://github.com/avi2d/checks/pull/67)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Release workflow
|
|
47
|
+
|
|
48
|
+
Each consumer owns `.github/workflows/release.yml` and calls the kit bins from it.
|
|
49
|
+
The version lives in `package.json`, and the tag names the same version with a leading `v`.
|
|
50
|
+
The build writes the changelog, so the release pull request carries the notes before the tag exists.
|
|
51
|
+
A repository that publishes a package to npm releases with this workflow:
|
|
52
|
+
|
|
53
|
+
```yaml
|
|
54
|
+
name: release
|
|
55
|
+
on:
|
|
56
|
+
push:
|
|
57
|
+
tags: ["v*"]
|
|
58
|
+
permissions:
|
|
59
|
+
contents: read
|
|
60
|
+
id-token: write
|
|
61
|
+
jobs:
|
|
62
|
+
publish:
|
|
63
|
+
runs-on: ubuntu-latest
|
|
64
|
+
steps:
|
|
65
|
+
- uses: actions/checkout@v5
|
|
66
|
+
with:
|
|
67
|
+
fetch-depth: 0
|
|
68
|
+
- uses: oven-sh/setup-bun@v2
|
|
69
|
+
- run: bun install --frozen-lockfile
|
|
70
|
+
- run: bun run build
|
|
71
|
+
- run: git diff --exit-code
|
|
72
|
+
- name: tag matches package version
|
|
73
|
+
run: test "v$(bun -p "require('./package.json').version")" = "$GITHUB_REF_NAME"
|
|
74
|
+
- uses: actions/setup-node@v4
|
|
75
|
+
with:
|
|
76
|
+
node-version: 24
|
|
77
|
+
registry-url: https://registry.npmjs.org
|
|
78
|
+
- run: npm publish
|
|
79
|
+
github-release:
|
|
80
|
+
needs: publish
|
|
81
|
+
runs-on: ubuntu-latest
|
|
82
|
+
permissions:
|
|
83
|
+
contents: write
|
|
84
|
+
steps:
|
|
85
|
+
- uses: actions/checkout@v5
|
|
86
|
+
with:
|
|
87
|
+
fetch-depth: 0
|
|
88
|
+
- uses: oven-sh/setup-bun@v2
|
|
89
|
+
- run: bun install --frozen-lockfile
|
|
90
|
+
- name: extract release notes
|
|
91
|
+
run: ./node_modules/.bin/checks-release-notes "${GITHUB_REF_NAME#v}" "$RUNNER_TEMP/release-notes.md"
|
|
92
|
+
- name: create the GitHub release
|
|
93
|
+
env:
|
|
94
|
+
GH_TOKEN: ${{ github.token }}
|
|
95
|
+
run: gh release create "$GITHUB_REF_NAME" --title "$GITHUB_REF_NAME" --notes-file "$RUNNER_TEMP/release-notes.md"
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
A repository that never publishes to npm releases with this workflow instead, since only `checks` publishes a package:
|
|
99
|
+
|
|
100
|
+
```yaml
|
|
101
|
+
name: release
|
|
102
|
+
on:
|
|
103
|
+
push:
|
|
104
|
+
tags: ["v*"]
|
|
105
|
+
permissions:
|
|
106
|
+
contents: read
|
|
107
|
+
jobs:
|
|
108
|
+
github-release:
|
|
109
|
+
runs-on: ubuntu-latest
|
|
110
|
+
permissions:
|
|
111
|
+
contents: write
|
|
112
|
+
steps:
|
|
113
|
+
- uses: actions/checkout@v5
|
|
114
|
+
with:
|
|
115
|
+
fetch-depth: 0
|
|
116
|
+
- uses: oven-sh/setup-bun@v2
|
|
117
|
+
- run: bun install --frozen-lockfile
|
|
118
|
+
- run: bun run build
|
|
119
|
+
- run: git diff --exit-code
|
|
120
|
+
- name: tag matches package version
|
|
121
|
+
run: test "v$(bun -p "require('./package.json').version")" = "$GITHUB_REF_NAME"
|
|
122
|
+
- name: extract release notes
|
|
123
|
+
run: ./node_modules/.bin/checks-release-notes "${GITHUB_REF_NAME#v}" "$RUNNER_TEMP/release-notes.md"
|
|
124
|
+
- name: create the GitHub release
|
|
125
|
+
env:
|
|
126
|
+
GH_TOKEN: ${{ github.token }}
|
|
127
|
+
run: gh release create "$GITHUB_REF_NAME" --title "$GITHUB_REF_NAME" --notes-file "$RUNNER_TEMP/release-notes.md"
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Cut a release by merging a pull request that holds only the version bump and the built changelog, then tagging the merge commit on the target branch and pushing the tag.
|
|
131
|
+
The workflow refuses a tag that disagrees with `package.json`, so the tag always names the section the notes come from.
|
|
132
|
+
|
|
133
|
+
## Opting out
|
|
134
|
+
|
|
135
|
+
Nothing runs it but the release workflow of a repository that publishes GitHub releases.
|
|
136
|
+
A repository with no versioned releases leaves it out.
|
|
137
|
+
|
|
138
|
+
## Related topics
|
|
139
|
+
|
|
140
|
+
- [checks-changelog](checks-changelog.md)
|
|
141
|
+
- [checks-release-report](checks-release-report.md)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: reference
|
|
3
|
+
audience: consumers
|
|
4
|
+
---
|
|
5
|
+
# checks-release-report
|
|
6
|
+
|
|
7
|
+
`checks-release-report` tells whether the history holds unreleased features or fixes since the last tag.
|
|
8
|
+
|
|
9
|
+
## What it checks
|
|
10
|
+
|
|
11
|
+
It lists the conventional commits after the last tag reachable from `HEAD` that reads as a version such as `v0.2.0`.
|
|
12
|
+
It counts a commit when its subject falls in Features, Fixes, Performance, Reverts or Breaking changes, the groups `checks-changelog` writes.
|
|
13
|
+
It prints each unreleased subject on its own line under a count.
|
|
14
|
+
A repository with no tag yet reports every such commit in its history.
|
|
15
|
+
A history with no conventional release-worthy commit reports no unreleased changes.
|
|
16
|
+
|
|
17
|
+
## What it reads
|
|
18
|
+
|
|
19
|
+
It reads the tags and the commit subjects from the git history.
|
|
20
|
+
It refuses a shallow checkout, since the tag it sees may not be the last one.
|
|
21
|
+
It passes over any other tag, since only a release tag reads as a version.
|
|
22
|
+
|
|
23
|
+
## Arguments
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
checks-release-report
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
It takes no arguments.
|
|
30
|
+
Run it before cutting a tag to decide whether a release is due.
|
|
31
|
+
|
|
32
|
+
## Exit codes
|
|
33
|
+
|
|
34
|
+
| Code | Result |
|
|
35
|
+
| --- | --- |
|
|
36
|
+
| 0 | No unreleased changes were found. |
|
|
37
|
+
| 1 | Unreleased changes were found. |
|
|
38
|
+
| 2 | The arguments do not parse, or the history cannot be read. |
|
|
39
|
+
|
|
40
|
+
## Sample output
|
|
41
|
+
|
|
42
|
+
A history with unreleased changes prints the count and each subject:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
release-report: 2 unreleased change(s) since v0.1.0:
|
|
46
|
+
feat: price a bill (#4)
|
|
47
|
+
fix(parts): keep the order of parts (#3)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
A history with nothing to release prints one line:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
release-report: no unreleased changes since v0.1.0
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Opting out
|
|
57
|
+
|
|
58
|
+
Nothing runs it but a person or a scheduler deciding when to cut a release.
|
|
59
|
+
A repository with no versioned releases leaves it out.
|
|
60
|
+
|
|
61
|
+
## Related topics
|
|
62
|
+
|
|
63
|
+
- [checks-changelog](checks-changelog.md)
|
|
64
|
+
- [checks-release-notes](checks-release-notes.md)
|