@avi2dg/checks 0.15.0 → 0.17.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 +19 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +5 -2
- package/dependency-cruiser.config.js +8 -0
- package/dist/feature-rules.js +139 -71
- package/dist/index.js +426 -7
- package/docs/configs/dependency-rules.md +1 -0
- package/docs/configs/effect-rules.md +1 -0
- package/docs/configs/quality-file.md +3 -3
- package/docs/configs/typescript-rules.md +36 -0
- package/docs/design.md +10 -1
- package/docs/gates/checks-comment-gate.md +2 -0
- package/docs/gates/checks-lint.md +4 -3
- package/docs/gates/checks-quality.md +1 -1
- package/docs/gates/checks-repetition.md +72 -0
- package/docs/gates/checks-size-budget.md +57 -14
- package/oxlintrc.json +1 -0
- package/package.json +7 -1
- package/quality.schema.json +63 -18
- package/scripts/comment-matchers.ts +19 -1
- package/scripts/gates.ts +1 -0
- package/scripts/git.ts +19 -1
- package/scripts/quality-file.ts +3 -17
- package/scripts/quality.ts +2 -2
- package/scripts/repetition.ts +161 -0
- package/scripts/size-budget.ts +191 -87
- package/scripts/size-rules.ts +126 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
Every release of `@avi2dg/checks`, newest first, written by the release from its conventional commits.
|
|
4
4
|
|
|
5
|
+
## 0.17.0
|
|
6
|
+
|
|
7
|
+
Released 2026-09-25.
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- **effect-channel:** add a cognitive complexity rule and make it the size budget's (#50)
|
|
12
|
+
- **scripts:** export the refused directive names from comment-matchers (#49)
|
|
13
|
+
- refuse undeclared package imports and deprecated symbol use (#48)
|
|
14
|
+
|
|
15
|
+
## 0.16.0
|
|
16
|
+
|
|
17
|
+
Released 2026-09-25.
|
|
18
|
+
|
|
19
|
+
### Features
|
|
20
|
+
|
|
21
|
+
- **scripts:** add checks-repetition to hold new repetition in production code (#46)
|
|
22
|
+
- **scripts:** add the recommended size limits, a tests budget and an overrun ratchet (#45)
|
|
23
|
+
|
|
5
24
|
## 0.15.0
|
|
6
25
|
|
|
7
26
|
Released 2026-09-25.
|
package/CONTRIBUTING.md
CHANGED
|
@@ -65,7 +65,7 @@ To place a change:
|
|
|
65
65
|
| Path | What it holds |
|
|
66
66
|
| --- | --- |
|
|
67
67
|
| `scripts/` | every bin, and the modules they share |
|
|
68
|
-
| `effect-channel/` | the Effect error-channel
|
|
68
|
+
| `effect-channel/` | the oxlint plugin with the Effect error-channel and cognitive complexity rules |
|
|
69
69
|
| `dist/` | the committed bundles of the plugin and of `featureRules` |
|
|
70
70
|
| `presets/` | the Effect presets `checks-quality` builds its fragments from |
|
|
71
71
|
| `templates/` | one template per kind of doc file, which `bun run build` renders |
|
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ A repository declares what it opts into once, in `quality.json`, and each check
|
|
|
16
16
|
- `@swc/core` 1.16.2
|
|
17
17
|
- `dependency-cruiser` 18.4.0
|
|
18
18
|
- `effect` 4.0.0-rc.115
|
|
19
|
+
- `jscpd` 5.3.2
|
|
19
20
|
- `oxlint` 1.83.0
|
|
20
21
|
- `oxlint-tsgolint` 7.0.2002
|
|
21
22
|
|
|
@@ -30,7 +31,7 @@ To consume the kit from a repository:
|
|
|
30
31
|
<!-- generated install: bun run build writes it from package.json and scripts/doc-blocks.ts -->
|
|
31
32
|
|
|
32
33
|
```sh
|
|
33
|
-
bun add -d @avi2dg/checks @effect/tsgo@0.45.0 @swc/core@1.16.2 dependency-cruiser@18.4.0 effect@4.0.0-rc.115 oxlint@1.83.0 oxlint-tsgolint@7.0.2002 typescript@7.0.2
|
|
34
|
+
bun add -d @avi2dg/checks @effect/tsgo@0.45.0 @swc/core@1.16.2 dependency-cruiser@18.4.0 effect@4.0.0-rc.115 jscpd@5.3.2 oxlint@1.83.0 oxlint-tsgolint@7.0.2002 typescript@7.0.2
|
|
34
35
|
```
|
|
35
36
|
|
|
36
37
|
<!-- end generated install -->
|
|
@@ -120,6 +121,7 @@ A repository leaves out a gate that does not apply to it through `gates.lint`, a
|
|
|
120
121
|
| [`checks-docs`](docs/gates/checks-docs.md) | the range | every repository |
|
|
121
122
|
| [`checks-quality`](docs/gates/checks-quality.md) | the working tree | a repository tracking `quality.json` |
|
|
122
123
|
| [`checks-size-budget`](docs/gates/checks-size-budget.md) | the range | a repository tracking `*.ts` or `*.tsx` |
|
|
124
|
+
| [`checks-repetition`](docs/gates/checks-repetition.md) | the range | a repository tracking `*.ts` or `*.tsx` |
|
|
123
125
|
| [`checks-feature-owners`](docs/gates/checks-feature-owners.md) | the range | a repository tracking `*.ts` or `*.tsx` |
|
|
124
126
|
|
|
125
127
|
<!-- end generated gates -->
|
|
@@ -168,7 +170,7 @@ Every path is relative to the installed package, `node_modules/@avi2dg/checks/`.
|
|
|
168
170
|
| `oxlintrc.json` | the oxlint base config `.oxlintrc.json` extends |
|
|
169
171
|
| `stryker.preset.js` | the Stryker mutation-testing preset |
|
|
170
172
|
| `tsconfig.effect.json` | the tsconfig fragment with the Effect language-service block |
|
|
171
|
-
| `dist/` | the compiled Effect error-channel
|
|
173
|
+
| `dist/` | the compiled oxlint plugin with the Effect error-channel and cognitive complexity rules, and `featureRules` |
|
|
172
174
|
|
|
173
175
|
<!-- end generated shipped -->
|
|
174
176
|
|
|
@@ -176,6 +178,7 @@ Every path is relative to the installed package, `node_modules/@avi2dg/checks/`.
|
|
|
176
178
|
|
|
177
179
|
- [The quality file](docs/configs/quality-file.md)
|
|
178
180
|
- [The Effect rules](docs/configs/effect-rules.md)
|
|
181
|
+
- [The TypeScript rules](docs/configs/typescript-rules.md)
|
|
179
182
|
- [The dependency rules](docs/configs/dependency-rules.md)
|
|
180
183
|
- [The commit message lint](docs/configs/commit-messages.md)
|
|
181
184
|
- [Why it is shaped this way](docs/design.md)
|
|
@@ -41,6 +41,14 @@ export default {
|
|
|
41
41
|
dependencyTypesNot: ["type-only", "npm-peer"],
|
|
42
42
|
},
|
|
43
43
|
},
|
|
44
|
+
{
|
|
45
|
+
name: "no-non-package-json",
|
|
46
|
+
severity: "error",
|
|
47
|
+
comment:
|
|
48
|
+
"The import resolves to an installed package the nearest package.json does not declare, so it holds only while something else keeps it hoisted. Declare it in dependencies, devDependencies or peerDependencies.",
|
|
49
|
+
from: {},
|
|
50
|
+
to: { dependencyTypes: ["npm-no-pkg", "npm-unknown"] },
|
|
51
|
+
},
|
|
44
52
|
{
|
|
45
53
|
name: "not-to-unresolvable",
|
|
46
54
|
severity: "error",
|
package/dist/feature-rules.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// scripts/feature-rules.ts
|
|
2
|
-
import { Schema as
|
|
2
|
+
import { Schema as Schema4 } from "effect";
|
|
3
3
|
|
|
4
4
|
// scripts/quality-file.ts
|
|
5
|
-
import { Console, Effect, FileSystem, JsonSchema, Path, Schema as
|
|
5
|
+
import { Console, Effect, FileSystem, JsonSchema, Path, Schema as Schema3 } from "effect";
|
|
6
6
|
|
|
7
7
|
// scripts/gates.ts
|
|
8
8
|
import { Schema } from "effect";
|
|
@@ -20,6 +20,7 @@ var KIT_GATES = [
|
|
|
20
20
|
{ bin: "checks-docs", script: "docs.ts", reads: "range", appliesTo: EVERY_REPOSITORY },
|
|
21
21
|
{ bin: "checks-quality", script: "quality.ts", reads: "tree", args: ["--check"], appliesTo: QUALITY_DECLARATION },
|
|
22
22
|
{ bin: "checks-size-budget", script: "size-budget.ts", reads: "range", appliesTo: TYPESCRIPT_SOURCE },
|
|
23
|
+
{ bin: "checks-repetition", script: "repetition.ts", reads: "range", appliesTo: TYPESCRIPT_SOURCE },
|
|
23
24
|
{ bin: "checks-feature-owners", script: "feature-owners.ts", reads: "range", appliesTo: TYPESCRIPT_SOURCE }
|
|
24
25
|
];
|
|
25
26
|
var UNCONDITIONAL = KIT_GATES.filter((gate) => gate.appliesTo === EVERY_REPOSITORY).map((gate) => gate.bin);
|
|
@@ -31,87 +32,154 @@ var LintGates = Schema.Array(Schema.Literals(KIT_GATES.map((gate) => gate.bin)))
|
|
|
31
32
|
return `checks-lint must run ${missing.join(", ")}, which ${verb} to ${EVERY_REPOSITORY}`;
|
|
32
33
|
}, { toJsonSchema: () => ({ allOf: UNCONDITIONAL.map((bin) => ({ contains: { const: bin } })) }) }));
|
|
33
34
|
|
|
35
|
+
// scripts/size-rules.ts
|
|
36
|
+
import { Schema as Schema2 } from "effect";
|
|
37
|
+
var TESTS_DIRECTORY = "tests";
|
|
38
|
+
var COUNTED = { skipBlankLines: false, skipComments: false };
|
|
39
|
+
var FILE_LINES = {
|
|
40
|
+
key: "fileLines",
|
|
41
|
+
rule: "max-lines",
|
|
42
|
+
options: COUNTED,
|
|
43
|
+
measured: /has too many lines \((\d+)\)/,
|
|
44
|
+
limits: "The most lines a file may hold, blank and comment lines counted"
|
|
45
|
+
};
|
|
46
|
+
var FUNCTION_LINES = {
|
|
47
|
+
key: "functionLines",
|
|
48
|
+
rule: "max-lines-per-function",
|
|
49
|
+
options: COUNTED,
|
|
50
|
+
measured: /has too many lines \((\d+)\)/,
|
|
51
|
+
limits: "The most lines a function may span, blank and comment lines counted"
|
|
52
|
+
};
|
|
53
|
+
var STATEMENTS = {
|
|
54
|
+
key: "statements",
|
|
55
|
+
rule: "max-statements",
|
|
56
|
+
options: {},
|
|
57
|
+
measured: /has too many statements \((\d+)\)/,
|
|
58
|
+
limits: "The most statements a function may hold"
|
|
59
|
+
};
|
|
60
|
+
var COMPLEXITY = {
|
|
61
|
+
key: "complexity",
|
|
62
|
+
rule: "cognitive-complexity",
|
|
63
|
+
plugin: "effect-channel",
|
|
64
|
+
options: {},
|
|
65
|
+
measured: /has a cognitive complexity of (\d+)/,
|
|
66
|
+
limits: "The highest cognitive complexity a function may reach, a switch counted once"
|
|
67
|
+
};
|
|
68
|
+
var DEPTH = {
|
|
69
|
+
key: "depth",
|
|
70
|
+
rule: "max-depth",
|
|
71
|
+
options: {},
|
|
72
|
+
measured: /nested too deeply \((\d+)\)/,
|
|
73
|
+
limits: "The deepest a block may nest inside a function"
|
|
74
|
+
};
|
|
75
|
+
var APPLIES = ["ratchet", "all"];
|
|
76
|
+
var SIZE_DEFAULTS = {
|
|
77
|
+
applies: "ratchet",
|
|
78
|
+
production: { fileLines: 400, functionLines: 100, statements: 30, complexity: 15, depth: 4 },
|
|
79
|
+
tests: { fileLines: 600, statements: 50, complexity: 15, depth: 4 }
|
|
80
|
+
};
|
|
81
|
+
var Limit = Schema2.Int.check(Schema2.isGreaterThan(0));
|
|
82
|
+
function limit({ limits }, fallback) {
|
|
83
|
+
return Schema2.optionalKey(Limit.annotate({ description: `${limits}; ${fallback} when absent` }));
|
|
84
|
+
}
|
|
85
|
+
var ProductionBudget = Schema2.Struct({
|
|
86
|
+
fileLines: limit(FILE_LINES, SIZE_DEFAULTS.production.fileLines),
|
|
87
|
+
functionLines: limit(FUNCTION_LINES, SIZE_DEFAULTS.production.functionLines),
|
|
88
|
+
statements: limit(STATEMENTS, SIZE_DEFAULTS.production.statements),
|
|
89
|
+
complexity: limit(COMPLEXITY, SIZE_DEFAULTS.production.complexity),
|
|
90
|
+
depth: limit(DEPTH, SIZE_DEFAULTS.production.depth)
|
|
91
|
+
});
|
|
92
|
+
var TestBudget = Schema2.Struct({
|
|
93
|
+
fileLines: limit(FILE_LINES, SIZE_DEFAULTS.tests.fileLines),
|
|
94
|
+
statements: limit(STATEMENTS, SIZE_DEFAULTS.tests.statements),
|
|
95
|
+
complexity: limit(COMPLEXITY, SIZE_DEFAULTS.tests.complexity),
|
|
96
|
+
depth: limit(DEPTH, SIZE_DEFAULTS.tests.depth)
|
|
97
|
+
});
|
|
98
|
+
var Size = Schema2.Struct({
|
|
99
|
+
applies: Schema2.optionalKey(Schema2.Literals(APPLIES).annotate({
|
|
100
|
+
description: `Which production and test files the budget holds: ratchet, the ones a range adds or changes, to no more overrun per rule than at the range's base; all, every one. ${SIZE_DEFAULTS.applies} when absent. The rest are listed as advisory`,
|
|
101
|
+
message: "Expected ratchet or all, and ratchet replaces changed"
|
|
102
|
+
})),
|
|
103
|
+
production: Schema2.optionalKey(ProductionBudget.annotate({
|
|
104
|
+
description: `The budget of the files under sources.production, and of the files listed as advisory outside ${TESTS_DIRECTORY}/`
|
|
105
|
+
})),
|
|
106
|
+
tests: Schema2.optionalKey(TestBudget.annotate({ description: `The budget of the files under ${TESTS_DIRECTORY}/, which sets no limit on a function's lines` }))
|
|
107
|
+
}).annotate({
|
|
108
|
+
description: "The size budget oxlint holds production and test files to, read by checks-size-budget",
|
|
109
|
+
messageUnexpectedKey: "Expected only applies, production and tests, since each limit is set inside production or tests"
|
|
110
|
+
});
|
|
111
|
+
|
|
34
112
|
// scripts/quality-file.ts
|
|
35
113
|
var SEGMENT = String.raw`(?!\.\.?(?:/|$))(?:\*\*|(?:[\w.@+-]|\*(?!\*))+)`;
|
|
36
114
|
var FILE = String.raw`(?:[\w.@+-]|\*(?!\*))*\.\w+`;
|
|
37
|
-
var PathGlob =
|
|
115
|
+
var PathGlob = Schema3.String.check(Schema3.isPattern(new RegExp(`^${SEGMENT}(?:/${SEGMENT})*/${FILE}$`), {
|
|
38
116
|
expected: "a glob from the repository root such as src/**/*.ts: a directory first, * within a segment, ** as a whole one, a file name with an extension last"
|
|
39
117
|
})).annotate({
|
|
40
118
|
identifier: "PathGlob",
|
|
41
119
|
description: "A glob from the repository root that oxlint, the Effect language service and git read alike: a directory first, * within a segment, ** as a whole one, a file name with an extension last, and no braces, ?, [ or leading ./"
|
|
42
120
|
});
|
|
43
|
-
var DocGlob =
|
|
121
|
+
var DocGlob = Schema3.String.check(Schema3.isPattern(new RegExp(`^(?:${SEGMENT}/)*${FILE}$`), {
|
|
44
122
|
expected: "a glob from the repository root such as README.md or docs/**/*.md: * within a segment, ** as a whole one, a file name with an extension last"
|
|
45
123
|
})).annotate({
|
|
46
124
|
identifier: "DocGlob",
|
|
47
125
|
description: "A glob from the repository root that checks-docs reads: * within a segment, ** as a whole one, a file name with an extension last"
|
|
48
126
|
});
|
|
49
127
|
var LITERAL_SEGMENT = String.raw`(?!\.\.?(?:/|$))[\w.@+-]+`;
|
|
50
|
-
var DirectoryPath =
|
|
128
|
+
var DirectoryPath = Schema3.String.check(Schema3.isPattern(new RegExp(`^${LITERAL_SEGMENT}(?:/${LITERAL_SEGMENT})*$`), {
|
|
51
129
|
expected: "a directory from the repository root such as src/billing, with no glob and no trailing slash"
|
|
52
130
|
})).annotate({ identifier: "DirectoryPath" });
|
|
53
|
-
var FilePath =
|
|
131
|
+
var FilePath = Schema3.String.check(Schema3.isPattern(new RegExp(`^(?:${LITERAL_SEGMENT}/)*[\\w.@+-]*\\.\\w+$`), {
|
|
54
132
|
expected: "a file from the repository root such as src/billing/index.ts, with no glob"
|
|
55
133
|
})).annotate({ identifier: "FilePath" });
|
|
56
134
|
var PROOF_DIRECTORY = "tests/e2e/";
|
|
57
|
-
var ProofPath =
|
|
135
|
+
var ProofPath = Schema3.String.check(Schema3.isPattern(new RegExp(`^${PROOF_DIRECTORY}(?:${LITERAL_SEGMENT}/)*[\\w.@+-]+\\.test\\.tsx?$`), {
|
|
58
136
|
expected: `a test file under ${PROOF_DIRECTORY} such as ${PROOF_DIRECTORY}billing.test.ts`
|
|
59
137
|
})).annotate({ identifier: "ProofPath" });
|
|
60
|
-
var Command =
|
|
61
|
-
var RuleName =
|
|
62
|
-
var Identity =
|
|
138
|
+
var Command = Schema3.NonEmptyString.annotate({ identifier: "Command" });
|
|
139
|
+
var RuleName = Schema3.String.check(Schema3.isPattern(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, { expected: "a Rule name in kebab case" })).annotate({ identifier: "RuleName" });
|
|
140
|
+
var Identity = Schema3.Struct({ name: Schema3.NonEmptyString, email: Schema3.NonEmptyString }).annotate({
|
|
63
141
|
identifier: "Identity"
|
|
64
142
|
});
|
|
65
|
-
var CommitIdentity =
|
|
66
|
-
authors:
|
|
143
|
+
var CommitIdentity = Schema3.Struct({
|
|
144
|
+
authors: Schema3.NonEmptyArray(Identity).annotate({
|
|
67
145
|
description: "The identities allowed to author and commit, in place of the kit's default owner"
|
|
68
146
|
})
|
|
69
147
|
});
|
|
70
|
-
var Gates =
|
|
71
|
-
ci:
|
|
148
|
+
var Gates = Schema3.Struct({
|
|
149
|
+
ci: Schema3.optionalKey(Schema3.NonEmptyArray(Command).annotate({
|
|
72
150
|
description: "The commands CI runs on every pull request to the default branch, each one plain command"
|
|
73
151
|
})),
|
|
74
|
-
scheduled:
|
|
75
|
-
lint:
|
|
152
|
+
scheduled: Schema3.optionalKey(Schema3.Array(Command).annotate({ description: "The commands a cron-scheduled workflow runs" })),
|
|
153
|
+
lint: Schema3.optionalKey(LintGates)
|
|
76
154
|
});
|
|
77
|
-
var EffectSources =
|
|
78
|
-
paths:
|
|
155
|
+
var EffectSources = Schema3.Struct({
|
|
156
|
+
paths: Schema3.NonEmptyArray(PathGlob).annotate({
|
|
79
157
|
description: "Where source is written in Effect, held to the Effect rules of oxlint and the language service"
|
|
80
158
|
}),
|
|
81
|
-
exempt:
|
|
159
|
+
exempt: Schema3.optionalKey(Schema3.Array(PathGlob).annotate({ description: "Files under paths the Effect rules pass over" }))
|
|
82
160
|
});
|
|
83
|
-
var Sources =
|
|
84
|
-
production:
|
|
85
|
-
effect:
|
|
86
|
-
});
|
|
87
|
-
var LineBudget = Schema2.Int.check(Schema2.isGreaterThan(0));
|
|
88
|
-
var Size = Schema2.Struct({
|
|
89
|
-
fileLines: LineBudget.annotate({ description: "The most lines a file may hold, blank and comment lines counted" }),
|
|
90
|
-
functionLines: LineBudget.annotate({
|
|
91
|
-
description: "The most lines a function may span, blank and comment lines counted"
|
|
92
|
-
}),
|
|
93
|
-
applies: Schema2.Literals(["changed", "all"]).annotate({
|
|
94
|
-
description: "Which production files the budget holds: changed, the ones a range adds or changes; all, every one. The rest are reported as advisory"
|
|
95
|
-
})
|
|
161
|
+
var Sources = Schema3.Struct({
|
|
162
|
+
production: Schema3.optionalKey(Schema3.Array(PathGlob).annotate({ description: "The source the repository ships, as against tests and tooling" })),
|
|
163
|
+
effect: Schema3.optionalKey(EffectSources)
|
|
96
164
|
});
|
|
97
|
-
var Feature =
|
|
98
|
-
name:
|
|
165
|
+
var Feature = Schema3.Struct({
|
|
166
|
+
name: Schema3.String.check(Schema3.isPattern(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, { expected: "a feature name in kebab case" })).annotate({ description: "The owner the dependency rule and the change signal name" }),
|
|
99
167
|
root: DirectoryPath.annotate({ description: "The directory the feature owns" }),
|
|
100
|
-
entries:
|
|
168
|
+
entries: Schema3.NonEmptyArray(FilePath).annotate({
|
|
101
169
|
description: "The files under root that code outside it imports the feature through"
|
|
102
170
|
}),
|
|
103
|
-
allowFrom:
|
|
171
|
+
allowFrom: Schema3.optionalKey(Schema3.Array(PathGlob).annotate({
|
|
104
172
|
description: "Files outside root that may import past its entries, such as a CLI or a harness; tests/ always may"
|
|
105
173
|
})),
|
|
106
174
|
proof: ProofPath.annotate({ description: "The end-to-end test that imports one of entries" })
|
|
107
|
-
}).check(
|
|
175
|
+
}).check(Schema3.makeFilter(({ root, entries }) => {
|
|
108
176
|
const outside = entries.filter((entry) => !entry.startsWith(`${root}/`));
|
|
109
177
|
return outside.length === 0 || `lists ${outside.join(", ")} among its entries, outside its root ${root}`;
|
|
110
178
|
}));
|
|
111
179
|
function nests(outer, inner) {
|
|
112
180
|
return outer === inner || inner.startsWith(`${outer}/`);
|
|
113
181
|
}
|
|
114
|
-
var Features =
|
|
182
|
+
var Features = Schema3.Array(Feature).check(Schema3.makeFilter((features) => {
|
|
115
183
|
const names = features.map((feature) => feature.name);
|
|
116
184
|
const repeated = names.filter((name, index) => names.indexOf(name) !== index);
|
|
117
185
|
if (repeated.length > 0)
|
|
@@ -123,16 +191,16 @@ var Features = Schema2.Array(Feature).check(Schema2.makeFilter((features) => {
|
|
|
123
191
|
}
|
|
124
192
|
return true;
|
|
125
193
|
}));
|
|
126
|
-
var AgentRules =
|
|
127
|
-
on:
|
|
128
|
-
off:
|
|
129
|
-
}).check(
|
|
194
|
+
var AgentRules = Schema3.Struct({
|
|
195
|
+
on: Schema3.optionalKey(Schema3.Array(RuleName).annotate({ description: "Catalogued Rules switched on here" })),
|
|
196
|
+
off: Schema3.optionalKey(Schema3.Array(RuleName).annotate({ description: "Catalogued Rules switched off here" }))
|
|
197
|
+
}).check(Schema3.makeFilter(({ on = [], off = [] }) => {
|
|
130
198
|
const both = on.filter((rule) => off.includes(rule));
|
|
131
199
|
return both.length === 0 || `switches ${both.join(", ")} both on and off`;
|
|
132
200
|
}));
|
|
133
|
-
var pagesIn = (mode) =>
|
|
134
|
-
var Docs =
|
|
135
|
-
pages:
|
|
201
|
+
var pagesIn = (mode) => Schema3.optionalKey(Schema3.Array(PathGlob).annotate({ description: `The pages written as ${mode}` }));
|
|
202
|
+
var Docs = Schema3.Struct({
|
|
203
|
+
pages: Schema3.optionalKey(Schema3.Struct({
|
|
136
204
|
tutorial: pagesIn("a tutorial, which teaches by building one thing"),
|
|
137
205
|
"how-to": pagesIn("a how-to, which walks one task"),
|
|
138
206
|
reference: pagesIn("reference, which describes a thing to be looked up"),
|
|
@@ -140,57 +208,57 @@ var Docs = Schema2.Struct({
|
|
|
140
208
|
}).annotate({
|
|
141
209
|
description: "The Diátaxis mode of each page, whose template checks-docs holds the page to; a page under docs/ needs one"
|
|
142
210
|
})),
|
|
143
|
-
forConsumers:
|
|
211
|
+
forConsumers: Schema3.optionalKey(Schema3.Array(DocGlob).annotate({
|
|
144
212
|
description: "The living docs that speak to a repository installing this one, whose bun run commands checks-docs does not hold to this package.json"
|
|
145
213
|
}))
|
|
146
214
|
});
|
|
147
|
-
var Quality =
|
|
148
|
-
$schema:
|
|
149
|
-
defaultBranch:
|
|
150
|
-
gates:
|
|
151
|
-
commitIdentity:
|
|
152
|
-
sources:
|
|
153
|
-
size:
|
|
154
|
-
features:
|
|
215
|
+
var Quality = Schema3.Struct({
|
|
216
|
+
$schema: Schema3.optionalKey(Schema3.String),
|
|
217
|
+
defaultBranch: Schema3.optionalKey(Schema3.NonEmptyString.annotate({ description: "The branch pull requests merge into; main when absent" })),
|
|
218
|
+
gates: Schema3.optionalKey(Gates),
|
|
219
|
+
commitIdentity: Schema3.optionalKey(CommitIdentity),
|
|
220
|
+
sources: Schema3.optionalKey(Sources),
|
|
221
|
+
size: Schema3.optionalKey(Size),
|
|
222
|
+
features: Schema3.optionalKey(Features.annotate({
|
|
155
223
|
description: "The feature owners dependency-cruiser holds to their entries and checks-feature-owners maps a change to"
|
|
156
224
|
})),
|
|
157
|
-
changeSignal:
|
|
225
|
+
changeSignal: Schema3.optionalKey(Schema3.Literal("advisory").annotate({
|
|
158
226
|
description: "Report which feature owners a change touches, without failing on it"
|
|
159
227
|
})),
|
|
160
|
-
agentRules:
|
|
161
|
-
docs:
|
|
228
|
+
agentRules: Schema3.optionalKey(AgentRules),
|
|
229
|
+
docs: Schema3.optionalKey(Docs.annotate({ description: "What checks-docs reads to map a doc file to its template" }))
|
|
162
230
|
}).annotate({
|
|
163
231
|
title: QUALITY_FILE,
|
|
164
232
|
description: "What a repository has opted into from @avi2dg/checks, read by its bins and agent Rule selection"
|
|
165
|
-
}).check(
|
|
233
|
+
}).check(Schema3.makeFilter(({ size, sources }) => size === undefined || (sources?.production ?? []).length > 0 || "declares size, which holds no production file without sources.production", {
|
|
166
234
|
toJsonSchema: () => ({
|
|
167
235
|
if: { required: ["size"] },
|
|
168
236
|
then: { required: ["sources"], properties: { sources: { required: ["production"], properties: { production: { minItems: 1 } } } } }
|
|
169
237
|
})
|
|
170
|
-
}),
|
|
238
|
+
}), Schema3.makeFilter(({ changeSignal, features = [] }) => changeSignal === undefined || features.length > 0 || "declares changeSignal, which maps a change to no owner without features", {
|
|
171
239
|
toJsonSchema: () => ({
|
|
172
240
|
if: { required: ["changeSignal"] },
|
|
173
241
|
then: { required: ["features"], properties: { features: { minItems: 1 } } }
|
|
174
242
|
})
|
|
175
243
|
}));
|
|
176
|
-
var LegacyManifest =
|
|
177
|
-
ciWiring:
|
|
178
|
-
gates:
|
|
179
|
-
scheduled:
|
|
180
|
-
lintGates:
|
|
181
|
-
defaultBranch:
|
|
244
|
+
var LegacyManifest = Schema3.Struct({
|
|
245
|
+
ciWiring: Schema3.optionalKey(Schema3.Struct({
|
|
246
|
+
gates: Schema3.optionalKey(Schema3.NonEmptyArray(Command)),
|
|
247
|
+
scheduled: Schema3.optionalKey(Schema3.Array(Command)),
|
|
248
|
+
lintGates: Schema3.optionalKey(LintGates),
|
|
249
|
+
defaultBranch: Schema3.optionalKey(Schema3.NonEmptyString)
|
|
182
250
|
})),
|
|
183
|
-
commitIdentity:
|
|
251
|
+
commitIdentity: Schema3.optionalKey(CommitIdentity)
|
|
184
252
|
});
|
|
185
253
|
var LEGACY_KEYS = ["ciWiring", "commitIdentity"];
|
|
186
254
|
|
|
187
|
-
class QualityUnreadable extends
|
|
188
|
-
message:
|
|
255
|
+
class QualityUnreadable extends Schema3.TaggedError()("QualityUnreadable", {
|
|
256
|
+
message: Schema3.String
|
|
189
257
|
}) {
|
|
190
258
|
}
|
|
191
259
|
var MANIFEST = "package.json";
|
|
192
|
-
var decodeQualityJson =
|
|
193
|
-
var decodeManifestJson =
|
|
260
|
+
var decodeQualityJson = Schema3.decodeUnknownEffect(Schema3.fromJsonString(Quality), { onExcessProperty: "error" });
|
|
261
|
+
var decodeManifestJson = Schema3.decodeUnknownEffect(Schema3.fromJsonString(LegacyManifest));
|
|
194
262
|
var decodeQuality = (text, source) => decodeQualityJson(text).pipe(Effect.mapError((cause) => new QualityUnreadable({ message: `${source}: ${cause.message}` })));
|
|
195
263
|
function fromLegacy({ ciWiring, commitIdentity }) {
|
|
196
264
|
const gates = {
|
|
@@ -254,7 +322,7 @@ function rulesFor(features) {
|
|
|
254
322
|
to: { path: `^${escaped(root)}/`, pathNot: entries.map((entry) => `^${escaped(entry)}$`) }
|
|
255
323
|
}));
|
|
256
324
|
}
|
|
257
|
-
var decode =
|
|
325
|
+
var decode = Schema4.decodeUnknownSync(Quality);
|
|
258
326
|
function featureRules(quality) {
|
|
259
327
|
return rulesFor(decode(quality, { onExcessProperty: "error" }).features ?? []);
|
|
260
328
|
}
|