@avi2dg/checks 0.25.0 → 0.27.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 +26 -0
- package/README.md +9 -10
- package/dist/data-shape/index.js +428 -0
- package/{templates → dist/templates}/reference.md +1 -1
- package/docs/configs/commit-messages.md +1 -1
- package/docs/configs/dependency-rules.md +7 -1
- package/docs/configs/effect-rules.md +12 -2
- package/docs/configs/native-settings.md +10 -31
- package/docs/configs/typescript-rules.md +43 -1
- package/docs/design.md +210 -104
- package/docs/gates/checks-changelog.md +8 -5
- package/docs/gates/checks-ci-wiring.md +2 -2
- package/docs/gates/checks-comment-gate.md +3 -4
- package/docs/gates/checks-commit-identity.md +3 -3
- package/docs/gates/checks-docs.md +67 -45
- package/docs/gates/checks-exports.md +104 -0
- package/docs/gates/checks-flake.md +3 -3
- package/docs/gates/checks-lint-coverage.md +17 -9
- package/docs/gates/checks-lint.md +8 -4
- package/docs/gates/checks-mutation-compare.md +3 -3
- package/docs/gates/checks-quarantine-clock.md +3 -5
- package/docs/gates/checks-release-notes.md +4 -4
- package/docs/gates/checks-release-report.md +3 -3
- package/docs/gates/checks-repetition.md +2 -1
- package/docs/gates/checks-subsumed-tests.md +3 -3
- package/docs/gates/checks-suppressions-ratchet.md +3 -4
- package/docs/gates/checks-test-layout.md +4 -4
- package/docs/gates/checks-test.md +3 -2
- package/docs/gates/checks-unused.md +4 -4
- package/docs/gates/checks-vendor.md +3 -3
- package/oxlintrc.json +22 -2
- package/package.json +17 -9
- package/src/complexity/exports.ts +188 -0
- package/src/complexity/knip.ts +70 -0
- package/src/complexity/unused.ts +7 -56
- package/src/core/gates.ts +2 -1
- package/src/core/swc.ts +1 -1
- package/src/delivery/ci-wiring.ts +1 -1
- package/src/dependencies/vendor.ts +4 -5
- package/src/docs/doc-names.ts +128 -0
- package/src/docs/doc-references.ts +1 -1
- package/src/docs/doc-snapshot.ts +1 -1
- package/src/docs/doc-templates.ts +4 -4
- package/src/docs/docs.ts +17 -14
- package/src/docs/prose-matchers.ts +11 -1
- package/src/quality/comments.ts +2 -2
- package/src/quality/lint-coverage.sh +35 -2
- package/{presets → src/quality/presets}/effect.language-service.json +3 -1
- package/src/testing/mutation-compare.ts +51 -45
- package/src/testing/test-layout.ts +1 -1
- package/src/testing/test-report.ts +1 -1
- package/ts-reset.d.ts +2 -0
- package/tsconfig.effect.json +3 -0
- /package/{templates → dist/templates}/adr.md +0 -0
- /package/{templates → dist/templates}/agents.md +0 -0
- /package/{templates → dist/templates}/changelog.md +0 -0
- /package/{templates → dist/templates}/claude.md +0 -0
- /package/{templates → dist/templates}/explanation.md +0 -0
- /package/{templates → dist/templates}/how-to.md +0 -0
- /package/{templates → dist/templates}/readme.md +0 -0
- /package/{templates → dist/templates}/tutorial.md +0 -0
- /package/{presets → src/quality/presets}/effect.oxlint.json +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
Every release of `@avi2dg/checks`, newest first, written by the release from its conventional commits.
|
|
4
4
|
|
|
5
|
+
## 0.27.0
|
|
6
|
+
|
|
7
|
+
Released 2026-09-27.
|
|
8
|
+
|
|
9
|
+
### Breaking changes
|
|
10
|
+
|
|
11
|
+
- **quality:** add data-shape oxlint plugin for readonly params and schema twins [#96](https://github.com/avi2d/checks/pull/96)
|
|
12
|
+
- ship stricter type gates for any, optional keys, process.env and ts-reset [#94](https://github.com/avi2d/checks/pull/94)
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
- **docs:** check agent files, vanished names and reports about the past [#95](https://github.com/avi2d/checks/pull/95)
|
|
17
|
+
|
|
18
|
+
### Fixes
|
|
19
|
+
|
|
20
|
+
- describe the kit without naming its owner in the package description [#91](https://github.com/avi2d/checks/pull/91)
|
|
21
|
+
|
|
22
|
+
## 0.26.0
|
|
23
|
+
|
|
24
|
+
Released 2026-09-27.
|
|
25
|
+
|
|
26
|
+
### Breaking changes
|
|
27
|
+
|
|
28
|
+
- **complexity:** add checks-exports gate holding unused exports to a shrinking baseline [#89](https://github.com/avi2d/checks/pull/89)
|
|
29
|
+
- move doc templates into dist/templates and presets into src/quality [#88](https://github.com/avi2d/checks/pull/88)
|
|
30
|
+
|
|
5
31
|
## 0.25.0
|
|
6
32
|
|
|
7
33
|
Released 2026-09-27.
|
package/README.md
CHANGED
|
@@ -10,7 +10,6 @@ Each repository owns its workflows and native tool configs, as [Native settings]
|
|
|
10
10
|
|
|
11
11
|
- A git repository, whose history the range gates read.
|
|
12
12
|
- Bun 1.3.13, which runs every bin.
|
|
13
|
-
- TypeScript 7.0.2, whose `tsc` the `typecheck` script runs.
|
|
14
13
|
- The peer dependencies, at the exact versions the kit pins:
|
|
15
14
|
- `@effect/tsgo` 0.45.0
|
|
16
15
|
- `@swc/core` 1.16.2
|
|
@@ -19,6 +18,7 @@ Each repository owns its workflows and native tool configs, as [Native settings]
|
|
|
19
18
|
- `jscpd` 5.3.2
|
|
20
19
|
- `oxlint` 1.83.0
|
|
21
20
|
- `oxlint-tsgolint` 7.0.2002
|
|
21
|
+
- `typescript` 7.0.2
|
|
22
22
|
|
|
23
23
|
<!-- end generated prerequisites -->
|
|
24
24
|
|
|
@@ -108,9 +108,9 @@ Add a pull request title lint step in another workflow using `./node_modules/.bi
|
|
|
108
108
|
|
|
109
109
|
## What runs
|
|
110
110
|
|
|
111
|
-
`checks-lint` runs
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
`checks-lint` runs each of these gates that applies to the repository, and names every one that fails.
|
|
112
|
+
A gate reads either the working tree or the range `checks-lint` resolves.
|
|
113
|
+
The table groups the gates by vector, the part of a repository each one judges.
|
|
114
114
|
|
|
115
115
|
<!-- generated gates: bun run build writes it from KIT_GATES in src/core/gates.ts and scripts/doc-blocks.ts -->
|
|
116
116
|
|
|
@@ -119,6 +119,7 @@ Each gate belongs to the vector it judges a repository on, and the table groups
|
|
|
119
119
|
| complexity | [`checks-suppressions-ratchet`](docs/gates/checks-suppressions-ratchet.md) | the range | every repository |
|
|
120
120
|
| complexity | [`checks-repetition`](docs/gates/checks-repetition.md) | the range | a repository tracking `*.ts` or `*.tsx` |
|
|
121
121
|
| complexity | [`checks-unused`](docs/gates/checks-unused.md) | the working tree | a repository tracking `*.ts` or `*.tsx` |
|
|
122
|
+
| complexity | [`checks-exports`](docs/gates/checks-exports.md) | the range | a repository tracking `*.ts` or `*.tsx` |
|
|
122
123
|
| quality | [`checks-lint-coverage`](docs/gates/checks-lint-coverage.md) | the working tree | a repository tracking `*.ts` or `*.tsx` |
|
|
123
124
|
| quality | [`checks-comment-gate`](docs/gates/checks-comment-gate.md) | the range | every repository |
|
|
124
125
|
| testing | [`checks-test-layout`](docs/gates/checks-test-layout.md) | the working tree | a repository tracking `*.ts` or `*.tsx` |
|
|
@@ -154,7 +155,6 @@ To move a repository to a newer release of the kit:
|
|
|
154
155
|
|
|
155
156
|
The repository's lockfile pins the kit, so a repository moves only when it runs these steps.
|
|
156
157
|
[CHANGELOG.md](CHANGELOG.md), shipped in the package, lists what each release changed.
|
|
157
|
-
A repository that tracks `quality.json` moves its settings as [Native settings](docs/configs/native-settings.md#consumer-migration) maps.
|
|
158
158
|
|
|
159
159
|
## Where things are
|
|
160
160
|
|
|
@@ -170,13 +170,12 @@ Every path is relative to the installed package, `node_modules/@avi2dg/checks/`.
|
|
|
170
170
|
| `commitlint.config.js` | the shared commitlint config |
|
|
171
171
|
| `dependency-cruiser.config.js` | the shared dependency-cruiser base |
|
|
172
172
|
| `knip-base.json` | the Knip base a repository's configuration imports |
|
|
173
|
-
| `src/` | every bin, which a package script calls by its `checks-` name,
|
|
174
|
-
| `
|
|
175
|
-
| `presets/` | the Effect rule blocks a repository copies into its native config |
|
|
173
|
+
| `src/` | every bin, which a package script calls by its `checks-` name, the modules the bins import, and the Effect rule blocks under `src/quality/presets/` |
|
|
174
|
+
| `dist/` | the compiled oxlint plugins and the doc templates, one template per kind of doc file |
|
|
176
175
|
| `oxlintrc.json` | the oxlint base config `.oxlintrc.json` extends |
|
|
177
176
|
| `stryker.preset.js` | the Stryker mutation-testing preset |
|
|
178
|
-
| `tsconfig.effect.json` | the tsconfig fragment with the Effect language-service block |
|
|
179
|
-
| `
|
|
177
|
+
| `tsconfig.effect.json` | the tsconfig fragment with the shared compiler options and the Effect language-service block |
|
|
178
|
+
| `ts-reset.d.ts` | the two ts-reset rules `tsconfig.effect.json` lists in `files` |
|
|
180
179
|
|
|
181
180
|
<!-- end generated shipped -->
|
|
182
181
|
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
// src/quality/data-shape/readonly-collection-param.ts
|
|
2
|
+
var ARRAY_METHODS = new Set([
|
|
3
|
+
"push",
|
|
4
|
+
"pop",
|
|
5
|
+
"shift",
|
|
6
|
+
"unshift",
|
|
7
|
+
"splice",
|
|
8
|
+
"sort",
|
|
9
|
+
"reverse",
|
|
10
|
+
"fill",
|
|
11
|
+
"copyWithin"
|
|
12
|
+
]);
|
|
13
|
+
var COLLECTION_METHODS = new Set(["set", "delete", "clear", "add"]);
|
|
14
|
+
var HANDOFF_PARENTS = new Set([
|
|
15
|
+
"ArrayExpression",
|
|
16
|
+
"ReturnStatement",
|
|
17
|
+
"YieldExpression",
|
|
18
|
+
"JSXExpressionContainer",
|
|
19
|
+
"JSXSpreadAttribute"
|
|
20
|
+
]);
|
|
21
|
+
var READONLY_NAMES = new Set(["ReadonlyArray", "ReadonlyMap", "ReadonlySet"]);
|
|
22
|
+
function isArrayType(type) {
|
|
23
|
+
if (type === undefined)
|
|
24
|
+
return false;
|
|
25
|
+
if (type.type === "TSArrayType")
|
|
26
|
+
return true;
|
|
27
|
+
return type.type === "TSTypeReference" && type.typeName.type === "Identifier" && type.typeName.name === "Array";
|
|
28
|
+
}
|
|
29
|
+
function isMapOrSet(type) {
|
|
30
|
+
return type?.type === "TSTypeReference" && type.typeName.type === "Identifier" && (type.typeName.name === "Map" || type.typeName.name === "Set");
|
|
31
|
+
}
|
|
32
|
+
function namedOf(param) {
|
|
33
|
+
if (param.type === "Identifier")
|
|
34
|
+
return { name: param.name, type: param.typeAnnotation?.typeAnnotation };
|
|
35
|
+
if (param.type === "AssignmentPattern" && param.left.type === "Identifier") {
|
|
36
|
+
return { name: param.left.name, type: param.left.typeAnnotation?.typeAnnotation };
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
function isReadonlyType(type) {
|
|
41
|
+
if (type?.type === "TSTypeOperator")
|
|
42
|
+
return type.operator === "readonly";
|
|
43
|
+
return type?.type === "TSTypeReference" && type.typeName.type === "Identifier" && READONLY_NAMES.has(type.typeName.name);
|
|
44
|
+
}
|
|
45
|
+
function declaredFunctions(program) {
|
|
46
|
+
const found = new Map;
|
|
47
|
+
for (const statement of program.body) {
|
|
48
|
+
const declaration = statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration" ? statement.declaration : statement;
|
|
49
|
+
if (declaration?.type === "FunctionDeclaration" && declaration.id !== null) {
|
|
50
|
+
found.set(declaration.id.name, declaration.params);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return found;
|
|
54
|
+
}
|
|
55
|
+
function isPassThrough(parent, child) {
|
|
56
|
+
if (parent === null)
|
|
57
|
+
return false;
|
|
58
|
+
if (parent.type === "ConditionalExpression")
|
|
59
|
+
return parent.test !== child;
|
|
60
|
+
return parent.type === "LogicalExpression" || parent.type === "AwaitExpression" || parent.type === "TSAsExpression" || parent.type === "TSTypeAssertion" || parent.type === "TSSatisfiesExpression" || parent.type === "TSNonNullExpression";
|
|
61
|
+
}
|
|
62
|
+
function valuePosition(node) {
|
|
63
|
+
let current = node;
|
|
64
|
+
while (isPassThrough(current.parent, current))
|
|
65
|
+
current = current.parent;
|
|
66
|
+
return current;
|
|
67
|
+
}
|
|
68
|
+
function writtenObjects(target) {
|
|
69
|
+
if (target === null)
|
|
70
|
+
return [];
|
|
71
|
+
if (target.type === "MemberExpression")
|
|
72
|
+
return target.object.type === "Identifier" ? [target.object.name] : [];
|
|
73
|
+
if (target.type === "ArrayPattern")
|
|
74
|
+
return target.elements.flatMap((element) => writtenObjects(element));
|
|
75
|
+
if (target.type === "ObjectPattern") {
|
|
76
|
+
return target.properties.flatMap((one) => writtenObjects(one.type === "Property" ? one.value : one));
|
|
77
|
+
}
|
|
78
|
+
if (target.type === "AssignmentPattern")
|
|
79
|
+
return writtenObjects(target.left);
|
|
80
|
+
if (target.type === "RestElement")
|
|
81
|
+
return writtenObjects(target.argument);
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
function calledMethod(callee) {
|
|
85
|
+
const { property } = callee;
|
|
86
|
+
if (!callee.computed && property.type === "Identifier")
|
|
87
|
+
return property.name;
|
|
88
|
+
if (property.type === "Literal" && typeof property.value === "string")
|
|
89
|
+
return property.value;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
function watch(params) {
|
|
93
|
+
const found = [];
|
|
94
|
+
for (const param of params) {
|
|
95
|
+
const named = namedOf(param);
|
|
96
|
+
if (named === undefined || named.type === undefined)
|
|
97
|
+
continue;
|
|
98
|
+
if (isArrayType(named.type))
|
|
99
|
+
found.push({ name: named.name, type: named.type, methods: ARRAY_METHODS });
|
|
100
|
+
else if (isMapOrSet(named.type))
|
|
101
|
+
found.push({ name: named.name, type: named.type, methods: COLLECTION_METHODS });
|
|
102
|
+
}
|
|
103
|
+
return found;
|
|
104
|
+
}
|
|
105
|
+
function fixOf(type, text) {
|
|
106
|
+
return type.type === "TSArrayType" ? `readonly ${text}` : `Readonly${text}`;
|
|
107
|
+
}
|
|
108
|
+
var rule = {
|
|
109
|
+
meta: {
|
|
110
|
+
type: "problem",
|
|
111
|
+
docs: { description: "Type a collection parameter the function never mutates as readonly" }
|
|
112
|
+
},
|
|
113
|
+
create(context) {
|
|
114
|
+
const stack = [];
|
|
115
|
+
let functions = new Map;
|
|
116
|
+
const mark = (name, method) => {
|
|
117
|
+
for (let index = stack.length - 1;index >= 0; index--) {
|
|
118
|
+
const frame = stack[index];
|
|
119
|
+
if (frame === undefined)
|
|
120
|
+
continue;
|
|
121
|
+
const param = frame.watched.find((one) => one.name === name);
|
|
122
|
+
if (param === undefined)
|
|
123
|
+
continue;
|
|
124
|
+
if (method === undefined || param.methods.has(method))
|
|
125
|
+
frame.mutated.add(name);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
const enter = (node) => {
|
|
130
|
+
stack.push({ watched: watch(node.body === null ? [] : node.params), mutated: new Set });
|
|
131
|
+
};
|
|
132
|
+
const leave = () => {
|
|
133
|
+
const frame = stack.pop();
|
|
134
|
+
if (frame === undefined)
|
|
135
|
+
return;
|
|
136
|
+
for (const param of frame.watched) {
|
|
137
|
+
if (frame.mutated.has(param.name))
|
|
138
|
+
continue;
|
|
139
|
+
const text = context.sourceCode.getText(param.type);
|
|
140
|
+
context.report({
|
|
141
|
+
node: param.type,
|
|
142
|
+
message: `parameter \`${param.name}\` is typed \`${text}\` but this function never mutates it. Type it \`${fixOf(param.type, text)}\` instead.`
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
const call = (node) => {
|
|
147
|
+
const { callee } = node;
|
|
148
|
+
if (callee.type !== "MemberExpression")
|
|
149
|
+
return;
|
|
150
|
+
const { object } = callee;
|
|
151
|
+
if (object.type !== "Identifier")
|
|
152
|
+
return;
|
|
153
|
+
mark(object.name, calledMethod(callee));
|
|
154
|
+
};
|
|
155
|
+
const passedReadonly = (site, argument) => {
|
|
156
|
+
if (site.callee.type !== "Identifier")
|
|
157
|
+
return false;
|
|
158
|
+
const params = functions.get(site.callee.name);
|
|
159
|
+
const index = site.arguments.findIndex((one) => one === argument);
|
|
160
|
+
const param = params?.[index];
|
|
161
|
+
return param !== undefined && isReadonlyType(namedOf(param)?.type);
|
|
162
|
+
};
|
|
163
|
+
const escapes = (node) => {
|
|
164
|
+
const value = valuePosition(node);
|
|
165
|
+
const { parent } = value;
|
|
166
|
+
if (parent === null)
|
|
167
|
+
return false;
|
|
168
|
+
if (parent.type === "CallExpression" || parent.type === "NewExpression") {
|
|
169
|
+
return parent.callee !== value && !passedReadonly(parent, value);
|
|
170
|
+
}
|
|
171
|
+
if (parent.type === "VariableDeclarator")
|
|
172
|
+
return parent.init === value;
|
|
173
|
+
if (parent.type === "AssignmentExpression" || parent.type === "AssignmentPattern")
|
|
174
|
+
return parent.right === value;
|
|
175
|
+
if (parent.type === "Property" || parent.type === "PropertyDefinition")
|
|
176
|
+
return parent.value === value;
|
|
177
|
+
if (parent.type === "ArrowFunctionExpression")
|
|
178
|
+
return parent.body === value;
|
|
179
|
+
return HANDOFF_PARENTS.has(parent.type);
|
|
180
|
+
};
|
|
181
|
+
const written = (target) => {
|
|
182
|
+
for (const name of writtenObjects(target))
|
|
183
|
+
mark(name, undefined);
|
|
184
|
+
};
|
|
185
|
+
return {
|
|
186
|
+
Program: (node) => {
|
|
187
|
+
functions = declaredFunctions(node);
|
|
188
|
+
},
|
|
189
|
+
Identifier: (node) => {
|
|
190
|
+
if (escapes(node))
|
|
191
|
+
mark(node.name, undefined);
|
|
192
|
+
},
|
|
193
|
+
FunctionDeclaration: enter,
|
|
194
|
+
FunctionExpression: enter,
|
|
195
|
+
ArrowFunctionExpression: enter,
|
|
196
|
+
"FunctionDeclaration:exit": leave,
|
|
197
|
+
"FunctionExpression:exit": leave,
|
|
198
|
+
"ArrowFunctionExpression:exit": leave,
|
|
199
|
+
CallExpression: call,
|
|
200
|
+
AssignmentExpression: (node) => {
|
|
201
|
+
written(node.left);
|
|
202
|
+
},
|
|
203
|
+
UpdateExpression: (node) => {
|
|
204
|
+
written(node.argument);
|
|
205
|
+
},
|
|
206
|
+
UnaryExpression: (node) => {
|
|
207
|
+
if (node.operator === "delete" && node.argument.type === "MemberExpression")
|
|
208
|
+
written(node.argument);
|
|
209
|
+
},
|
|
210
|
+
ForOfStatement: (node) => {
|
|
211
|
+
written(node.left);
|
|
212
|
+
},
|
|
213
|
+
ForInStatement: (node) => {
|
|
214
|
+
written(node.left);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
var readonly_collection_param_default = rule;
|
|
220
|
+
|
|
221
|
+
// src/quality/data-shape/schema-twin.ts
|
|
222
|
+
var STRUCTS = new Set(["Struct", "TaggedStruct", "Class"]);
|
|
223
|
+
var REFINEMENTS = new Set(["check", "annotate"]);
|
|
224
|
+
var SCHEMA_KINDS = {
|
|
225
|
+
String: "string",
|
|
226
|
+
NonEmptyString: "string",
|
|
227
|
+
Trimmed: "string",
|
|
228
|
+
Literal: "literal",
|
|
229
|
+
Literals: "literal",
|
|
230
|
+
Number: "number",
|
|
231
|
+
Finite: "number",
|
|
232
|
+
Int: "number",
|
|
233
|
+
NonNegativeInt: "number",
|
|
234
|
+
Boolean: "boolean",
|
|
235
|
+
Array: "array",
|
|
236
|
+
NonEmptyArray: "array",
|
|
237
|
+
Struct: "object",
|
|
238
|
+
TaggedStruct: "object",
|
|
239
|
+
Record: "record",
|
|
240
|
+
Unknown: "unknown"
|
|
241
|
+
};
|
|
242
|
+
function keyName(key) {
|
|
243
|
+
if (key.type === "Identifier")
|
|
244
|
+
return key.name;
|
|
245
|
+
if (key.type === "Literal" && typeof key.value === "string")
|
|
246
|
+
return key.value;
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
function typeKind(type) {
|
|
250
|
+
const value = type?.type === "TSTypeOperator" ? type.typeAnnotation : type;
|
|
251
|
+
if (value === undefined)
|
|
252
|
+
return "wild";
|
|
253
|
+
if (value.type === "TSStringKeyword")
|
|
254
|
+
return "string";
|
|
255
|
+
if (value.type === "TSNumberKeyword")
|
|
256
|
+
return "number";
|
|
257
|
+
if (value.type === "TSBooleanKeyword")
|
|
258
|
+
return "boolean";
|
|
259
|
+
if (value.type === "TSArrayType")
|
|
260
|
+
return "array";
|
|
261
|
+
if (value.type === "TSTypeLiteral")
|
|
262
|
+
return "object";
|
|
263
|
+
if (value.type === "TSLiteralType")
|
|
264
|
+
return "literal";
|
|
265
|
+
if (value.type === "TSUnknownKeyword")
|
|
266
|
+
return "unknown";
|
|
267
|
+
if (value.type === "TSTypeReference")
|
|
268
|
+
return referenceKind(value);
|
|
269
|
+
if (value.type === "TSUnionType" && value.types.every((one) => one.type === "TSLiteralType"))
|
|
270
|
+
return "literal";
|
|
271
|
+
return "wild";
|
|
272
|
+
}
|
|
273
|
+
function referenceKind(value) {
|
|
274
|
+
if (value.typeName.type !== "Identifier")
|
|
275
|
+
return "wild";
|
|
276
|
+
if (value.typeName.name === "Array" || value.typeName.name === "ReadonlyArray")
|
|
277
|
+
return "array";
|
|
278
|
+
if (value.typeName.name === "Record" || value.typeName.name === "ReadonlyRecord")
|
|
279
|
+
return "record";
|
|
280
|
+
return "wild";
|
|
281
|
+
}
|
|
282
|
+
function schemaCallName(node) {
|
|
283
|
+
if (node === undefined)
|
|
284
|
+
return;
|
|
285
|
+
if (node.type === "CallExpression")
|
|
286
|
+
return schemaCallName(node.callee);
|
|
287
|
+
if (node.type !== "MemberExpression")
|
|
288
|
+
return;
|
|
289
|
+
const { object, property } = node;
|
|
290
|
+
if (object.type !== "Identifier" || object.name !== "Schema" || property.type !== "Identifier")
|
|
291
|
+
return;
|
|
292
|
+
return property.name;
|
|
293
|
+
}
|
|
294
|
+
function kindOf(name) {
|
|
295
|
+
return name === undefined ? "wild" : SCHEMA_KINDS[name] ?? "wild";
|
|
296
|
+
}
|
|
297
|
+
function refinedBase(value) {
|
|
298
|
+
const { callee } = value;
|
|
299
|
+
if (callee.type !== "MemberExpression" || callee.object.type === "Super")
|
|
300
|
+
return;
|
|
301
|
+
if (callee.property.type !== "Identifier" || !REFINEMENTS.has(callee.property.name))
|
|
302
|
+
return;
|
|
303
|
+
return callee.object;
|
|
304
|
+
}
|
|
305
|
+
function schemaKind(value) {
|
|
306
|
+
if (value !== undefined && value.type === "CallExpression") {
|
|
307
|
+
const name = schemaCallName(value.callee);
|
|
308
|
+
if (name === "optionalKey" || name === "optional") {
|
|
309
|
+
const first = value.arguments[0];
|
|
310
|
+
const inner = first === undefined || first.type === "SpreadElement" ? undefined : first;
|
|
311
|
+
return { optional: true, kind: schemaKind(inner).kind };
|
|
312
|
+
}
|
|
313
|
+
const base = refinedBase(value);
|
|
314
|
+
if (base !== undefined)
|
|
315
|
+
return schemaKind(base);
|
|
316
|
+
return { optional: false, kind: kindOf(name) };
|
|
317
|
+
}
|
|
318
|
+
return { optional: false, kind: kindOf(schemaCallName(value)) };
|
|
319
|
+
}
|
|
320
|
+
function shapeFields(node) {
|
|
321
|
+
const members = node.type === "TSTypeLiteral" ? node.members : node.body;
|
|
322
|
+
const found = [];
|
|
323
|
+
for (const member of members) {
|
|
324
|
+
if (member.type !== "TSPropertySignature")
|
|
325
|
+
continue;
|
|
326
|
+
const name = keyName(member.key);
|
|
327
|
+
if (name === undefined)
|
|
328
|
+
continue;
|
|
329
|
+
found.push({ name, optional: member.optional, kind: typeKind(member.typeAnnotation?.typeAnnotation) });
|
|
330
|
+
}
|
|
331
|
+
return found;
|
|
332
|
+
}
|
|
333
|
+
function schemaFields(call) {
|
|
334
|
+
const { callee } = call;
|
|
335
|
+
const inner = callee.type === "CallExpression" ? callee.callee : callee;
|
|
336
|
+
if (inner.type !== "MemberExpression")
|
|
337
|
+
return;
|
|
338
|
+
const { object, property } = inner;
|
|
339
|
+
if (object.type !== "Identifier" || object.name !== "Schema" || property.type !== "Identifier" || !STRUCTS.has(property.name)) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const fields = call.arguments.find((one) => one.type === "ObjectExpression");
|
|
343
|
+
if (fields === undefined)
|
|
344
|
+
return;
|
|
345
|
+
const found = property.name === "TaggedStruct" ? [{ name: "_tag", optional: false, kind: "literal" }] : [];
|
|
346
|
+
for (const prop of fields.properties) {
|
|
347
|
+
if (prop.type !== "Property" || prop.computed)
|
|
348
|
+
continue;
|
|
349
|
+
const name = keyName(prop.key);
|
|
350
|
+
if (name === undefined)
|
|
351
|
+
continue;
|
|
352
|
+
const { optional, kind } = schemaKind(prop.value);
|
|
353
|
+
found.push({ name, optional, kind });
|
|
354
|
+
}
|
|
355
|
+
return found.length >= 2 ? found : undefined;
|
|
356
|
+
}
|
|
357
|
+
function twins(typeFields, schema) {
|
|
358
|
+
return typeFields.length === schema.length && typeFields.every((field) => {
|
|
359
|
+
const match = schema.find((one) => one.name === field.name);
|
|
360
|
+
return match !== undefined && match.optional === field.optional && field.kind === match.kind;
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
function schemaName(call) {
|
|
364
|
+
const { parent } = call;
|
|
365
|
+
if (parent.type !== "VariableDeclarator" || parent.id.type !== "Identifier")
|
|
366
|
+
return;
|
|
367
|
+
return parent.id.name;
|
|
368
|
+
}
|
|
369
|
+
function shapeName(node) {
|
|
370
|
+
const { parent } = node;
|
|
371
|
+
if (parent.type !== "TSTypeAliasDeclaration" && parent.type !== "TSInterfaceDeclaration")
|
|
372
|
+
return;
|
|
373
|
+
return parent.id.name;
|
|
374
|
+
}
|
|
375
|
+
function describeTwin(type, schema) {
|
|
376
|
+
const subject = type === undefined ? "This object type" : `Type \`${type}\``;
|
|
377
|
+
if (schema === undefined)
|
|
378
|
+
return `${subject} repeats the fields of a Schema in this file. Derive it from the schema instead of writing both.`;
|
|
379
|
+
return `${subject} repeats the fields of schema \`${schema}\` in this file. Derive it as \`typeof ${schema}.Type\` instead of writing both.`;
|
|
380
|
+
}
|
|
381
|
+
var rule2 = {
|
|
382
|
+
meta: {
|
|
383
|
+
type: "problem",
|
|
384
|
+
docs: { description: "Derive an object type from the Schema it repeats instead of writing both" }
|
|
385
|
+
},
|
|
386
|
+
create(context) {
|
|
387
|
+
const shapes = [];
|
|
388
|
+
const schemas = [];
|
|
389
|
+
const collect = (node) => {
|
|
390
|
+
if (node.parent.type === "TSInterfaceDeclaration" && node.parent.extends.length > 0)
|
|
391
|
+
return;
|
|
392
|
+
const fields = shapeFields(node);
|
|
393
|
+
if (fields.length >= 2)
|
|
394
|
+
shapes.push({ node, fields });
|
|
395
|
+
};
|
|
396
|
+
return {
|
|
397
|
+
TSTypeLiteral: collect,
|
|
398
|
+
TSInterfaceBody: collect,
|
|
399
|
+
CallExpression(node) {
|
|
400
|
+
const fields = schemaFields(node);
|
|
401
|
+
if (fields !== undefined)
|
|
402
|
+
schemas.push({ name: schemaName(node), fields });
|
|
403
|
+
},
|
|
404
|
+
"Program:exit"() {
|
|
405
|
+
for (const shape of shapes) {
|
|
406
|
+
const twin = schemas.find((schema) => twins(shape.fields, schema.fields));
|
|
407
|
+
if (twin === undefined)
|
|
408
|
+
continue;
|
|
409
|
+
context.report({ node: shape.node, message: describeTwin(shapeName(shape.node), twin.name) });
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
var schema_twin_default = rule2;
|
|
416
|
+
|
|
417
|
+
// src/quality/data-shape/index.ts
|
|
418
|
+
var plugin = {
|
|
419
|
+
meta: { name: "data-shape" },
|
|
420
|
+
rules: {
|
|
421
|
+
"readonly-collection-param": readonly_collection_param_default,
|
|
422
|
+
"schema-twin": schema_twin_default
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
var data_shape_default = plugin;
|
|
426
|
+
export {
|
|
427
|
+
data_shape_default as default
|
|
428
|
+
};
|
|
@@ -4,7 +4,7 @@ audience: consumers
|
|
|
4
4
|
---
|
|
5
5
|
# The commit message lint
|
|
6
6
|
|
|
7
|
-
The shared commitlint config holds each pull request title to
|
|
7
|
+
The shared commitlint config holds each pull request title to the conventional commit format.
|
|
8
8
|
|
|
9
9
|
## Config
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@ audience: consumers
|
|
|
4
4
|
---
|
|
5
5
|
# The dependency rules
|
|
6
6
|
|
|
7
|
-
The shared dependency-cruiser base holds a repository's imports to a set of rules
|
|
7
|
+
The shared dependency-cruiser base holds a repository's imports to a set of rules, and a repository adds its own boundaries on top.
|
|
8
8
|
|
|
9
9
|
## Base rules
|
|
10
10
|
|
|
@@ -19,6 +19,12 @@ The shared dependency-cruiser base holds a repository's imports to a set of rule
|
|
|
19
19
|
|
|
20
20
|
The base parses with swc, so it needs `@swc/core` installed, and without it the cruise silently skips every `.ts` file.
|
|
21
21
|
|
|
22
|
+
`no-deep-imports` judges the import specifier, never the file it resolves to.
|
|
23
|
+
The base honours `exports` maps, so a subpath the map publishes resolves and passes, and a subpath it omits fails to resolve and is reported.
|
|
24
|
+
A package without an `exports` map publishes every file.
|
|
25
|
+
A bare import always passes, whatever file its entry lives in.
|
|
26
|
+
A repository that sets its own `options.enhancedResolveOptions` replaces the base's, and restates `exportsFields` and `conditionNames` in it.
|
|
27
|
+
|
|
22
28
|
## Boundaries
|
|
23
29
|
|
|
24
30
|
A repository appends a named rule per boundary it owns:
|
|
@@ -31,7 +31,7 @@ The repo's `.oxlintrc.json` owns its Effect paths and exemptions in an override:
|
|
|
31
31
|
}
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
The kit ships the rule block in `presets/effect.oxlint.json` for copying into the override.
|
|
34
|
+
The kit ships the rule block in `src/quality/presets/effect.oxlint.json` for copying into the override.
|
|
35
35
|
Each config in an oxlint `extends` chain sets `plugins` explicitly, because an omitted list enables defaults across the chain.
|
|
36
36
|
`unicorn/no-process-exit` does not check a shebang script, so a bin can use `eslint/no-restricted-properties` for `process.exit`.
|
|
37
37
|
|
|
@@ -39,7 +39,17 @@ Each config in an oxlint `extends` chain sets `plugins` explicitly, because an o
|
|
|
39
39
|
|
|
40
40
|
The repository's `tsconfig.json` holds its Effect override under `compilerOptions.plugins`.
|
|
41
41
|
The override includes the same source paths and excludes the same exempt paths as oxlint.
|
|
42
|
-
The kit ships severity values in `presets/effect.language-service.json` for the override's `options`.
|
|
42
|
+
The kit ships severity values in `src/quality/presets/effect.language-service.json` for the override's `options`.
|
|
43
|
+
The preset turns these diagnostics to errors:
|
|
44
|
+
|
|
45
|
+
- `nodeBuiltinImport` refuses an import of a Node built-in module that has an Effect counterpart.
|
|
46
|
+
- `asyncFunction` refuses an `async` function.
|
|
47
|
+
- `newPromise` refuses `new Promise`.
|
|
48
|
+
- `extendsNativeError` refuses a class that extends the native `Error` directly.
|
|
49
|
+
- `processEnv` refuses a read of `process.env` outside an Effect generator.
|
|
50
|
+
- `processEnvInEffect` refuses a read of `process.env` inside an Effect generator.
|
|
51
|
+
|
|
52
|
+
Effect's `Config` reads the environment in place of `process.env`.
|
|
43
53
|
The kit's `tsconfig.effect.json` keeps the shared language service diagnostics.
|
|
44
54
|
|
|
45
55
|
## Related topics
|
|
@@ -4,7 +4,7 @@ audience: consumers
|
|
|
4
4
|
---
|
|
5
5
|
# Native settings
|
|
6
6
|
|
|
7
|
-
A consuming repository puts each setting in the file its tool reads.
|
|
7
|
+
A consuming repository puts each setting in the file its tool reads, and no manifest of the kit gathers them.
|
|
8
8
|
|
|
9
9
|
## Settings by file
|
|
10
10
|
|
|
@@ -13,8 +13,9 @@ A consuming repository puts each setting in the file its tool reads.
|
|
|
13
13
|
| `.github/workflows/*.yml` | Pull request commands, scheduled commands, runner labels | GitHub Actions and `checks-ci-wiring` |
|
|
14
14
|
| `.oxlintrc.json` | Effect paths, exemptions, file size, function size, statements, cognitive complexity and depth | oxlint |
|
|
15
15
|
| `oxlint-suppressions.json` | The existing violations oxlint suppresses, per file and rule | oxlint and `checks-suppressions-ratchet` |
|
|
16
|
+
| `exports-baseline.json` | The existing unused exports and types, per file, kind and name | `checks-exports` |
|
|
16
17
|
| `.jscpd.json` | The `path` and `ignore` globs of the files repetition is measured in | jscpd and `checks-repetition` |
|
|
17
|
-
| `knip.config.ts` | The `entry` globs Knip traces unreferenced files from, spread over the kit's `knip-base.json` | Knip and `checks-
|
|
18
|
+
| `knip.config.ts` | The `entry` globs Knip traces unreferenced files from, spread over the kit's `knip-base.json` | Knip, `checks-unused` and `checks-exports` |
|
|
18
19
|
| `tsconfig.json` | Effect language service scope and severity | TypeScript and Effect language service |
|
|
19
20
|
| `package.json` | `scripts` with the `checks-vendor` arguments in `prepare`, `author` and `contributors` | Bun, `checks-commit-identity` and `checks-vendor` |
|
|
20
21
|
| `bunfig.toml` | Test discovery and quarantine exclusion | Bun and `checks-test-layout` |
|
|
@@ -23,37 +24,14 @@ A consuming repository puts each setting in the file its tool reads.
|
|
|
23
24
|
| Each page under `docs/` | Diátaxis mode in `kind` front matter, and `audience: consumers` on a page that speaks to a consuming repository | `checks-docs` |
|
|
24
25
|
|
|
25
26
|
Every page under `docs/` names its mode in `kind` front matter, whatever directory holds it.
|
|
26
|
-
A page whose front matter sets `audience: consumers` names commands a consuming repository runs
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
## Consumer migration
|
|
30
|
-
|
|
31
|
-
The following table maps the former fields to their owners.
|
|
32
|
-
|
|
33
|
-
| Former field | New owner |
|
|
34
|
-
| --- | --- |
|
|
35
|
-
| `defaultBranch` | Git's `refs/remotes/origin/HEAD`, or the pull request base or event repository in CI |
|
|
36
|
-
| `runsOn`, `gates.ci`, `gates.scheduled` | `runs-on` and `run` steps in `.github/workflows/*.yml` |
|
|
37
|
-
| `gates.lint` | `checks-lint` runs every kit gate |
|
|
38
|
-
| `commitIdentity.authors` | `author` and `contributors` in `package.json` |
|
|
39
|
-
| `sources.production` | `path` and `ignore` in `.jscpd.json` |
|
|
40
|
-
| `size.production`, `size.tests` | File overrides and native size rules in `.oxlintrc.json` |
|
|
41
|
-
| `sources.effect.paths`, `sources.effect.exempt` | Overrides in `.oxlintrc.json` and `tsconfig.json` |
|
|
42
|
-
| `sources.libraries` | `checks-vendor` arguments in the `prepare` script of `package.json` |
|
|
43
|
-
| `docs.pages` | `kind` front matter on each page |
|
|
44
|
-
| `docs.forConsumers` | `audience: consumers` front matter on each page |
|
|
45
|
-
| `features`, `changeSignal`, `agentRules` | No active declarations used these fields |
|
|
46
|
-
|
|
47
|
-
The kit has no general configuration manifest or generated workflow.
|
|
48
|
-
`checks-ci-wiring` requires title lint on opened and synchronized pull requests.
|
|
49
|
-
It also requires lint, build, typecheck and test for each of those scripts that `package.json` defines, and a clean git diff after a build.
|
|
50
|
-
`checks-repetition` runs jscpd with the repository's own `.jscpd.json`, so its `path` and `ignore` globs decide which files are measured.
|
|
27
|
+
A page whose front matter sets `audience: consumers` names commands that a consuming repository runs.
|
|
28
|
+
`checks-docs` skips the `bun run` commands on such a page, and resolves those in every other living doc or agent file as [Paths, links and commands](../gates/checks-docs.md#paths-links-and-commands) says.
|
|
51
29
|
|
|
52
30
|
## Size limits
|
|
53
31
|
|
|
54
|
-
The size rules are
|
|
32
|
+
The size rules are oxlint's own rules at `error` in `.oxlintrc.json`, so `bun run lint` fails on any file over them.
|
|
55
33
|
A repository records its existing violations with `oxlint --suppress-all`, which writes them to `oxlint-suppressions.json`.
|
|
56
|
-
`checks-suppressions-ratchet` refuses any count in that file that rises
|
|
34
|
+
`checks-suppressions-ratchet` refuses any count in that file that rises.
|
|
57
35
|
The kit's own `.oxlintrc.json` sets these limits for each size override, and a repository may copy them:
|
|
58
36
|
|
|
59
37
|
<!-- generated size-limits: bun run build writes it from .oxlintrc.json, SIZE_RULES in src/complexity/size-rules.ts and scripts/doc-blocks.ts -->
|
|
@@ -71,5 +49,6 @@ The kit's own `.oxlintrc.json` sets these limits for each size override, and a r
|
|
|
71
49
|
## Related topics
|
|
72
50
|
|
|
73
51
|
- [The Effect rules](effect-rules.md)
|
|
74
|
-
- [
|
|
75
|
-
- [
|
|
52
|
+
- [checks-ci-wiring](../gates/checks-ci-wiring.md)
|
|
53
|
+
- [checks-suppressions-ratchet](../gates/checks-suppressions-ratchet.md)
|
|
54
|
+
- [checks-docs](../gates/checks-docs.md)
|