covgate 0.0.0-stage → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +504 -2
  3. package/bin/covgate-postinstall.js +13 -0
  4. package/bin/covgate.js +5 -0
  5. package/docs/ai-workflow.md +197 -0
  6. package/docs/design.md +327 -0
  7. package/docs/setup.md +165 -0
  8. package/package.json +48 -4
  9. package/schema/covgate.schema.json +295 -0
  10. package/src/annotations.js +224 -0
  11. package/src/args.js +189 -0
  12. package/src/cli.js +486 -0
  13. package/src/config.js +442 -0
  14. package/src/coverage/index.js +85 -0
  15. package/src/coverage/istanbul.js +215 -0
  16. package/src/coverage/lcov.js +234 -0
  17. package/src/coverage/model.js +316 -0
  18. package/src/coverage/paths.js +40 -0
  19. package/src/errors.js +34 -0
  20. package/src/gate.js +203 -0
  21. package/src/glob.js +242 -0
  22. package/src/index.js +28 -0
  23. package/src/process.js +170 -0
  24. package/src/reason.js +77 -0
  25. package/src/report/build.js +135 -0
  26. package/src/report/index.js +29 -0
  27. package/src/report/markdown.js +343 -0
  28. package/src/report/text.js +267 -0
  29. package/src/reports.js +113 -0
  30. package/src/runner.js +95 -0
  31. package/src/scope.js +440 -0
  32. package/src/setup/agent.js +329 -0
  33. package/src/setup/config.js +129 -0
  34. package/src/setup/index.js +270 -0
  35. package/src/setup/project.js +391 -0
  36. package/src/setup/prompt.js +43 -0
  37. package/src/util.js +163 -0
  38. package/src/version.js +8 -0
  39. package/templates/AGENTS.md.snippet.md +10 -0
  40. package/templates/agent-stop-hook.sh +7 -0
  41. package/templates/covgate.config.json +98 -0
  42. package/templates/github-actions-coverage.yml +41 -0
  43. package/templates/gitignore +5 -0
  44. package/templates/pre-commit +10 -0
package/package.json CHANGED
@@ -1,6 +1,50 @@
1
1
  {
2
2
  "name": "covgate",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.1",
4
+ "description": "覆盖率门禁:客户端 100%,服务端至少 95%,安装时自动检测/配置测试框架与 AI 编码代理接入。",
5
+ "type": "module",
6
+ "bin": {
7
+ "covgate": "bin/covgate.js"
8
+ },
9
+ "exports": {
10
+ ".": "./src/index.js"
11
+ },
12
+ "files": [
13
+ "bin",
14
+ "src",
15
+ "schema",
16
+ "templates",
17
+ "docs",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "engines": {
22
+ "node": ">=20.11"
23
+ },
24
+ "scripts": {
25
+ "test": "node --test test",
26
+ "test:watch": "node --test --watch test",
27
+ "postinstall": "node bin/covgate-postinstall.js"
28
+ },
29
+ "keywords": [
30
+ "coverage",
31
+ "code-coverage",
32
+ "coverage-gate",
33
+ "ai",
34
+ "agent",
35
+ "vitest",
36
+ "jest",
37
+ "ci",
38
+ "quality-gate"
39
+ ],
40
+ "license": "MIT",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "git+https://gitee.com/wxzzhj/covgate.git"
44
+ },
45
+ "homepage": "https://gitee.com/wxzzhj/covgate",
46
+ "bugs": {
47
+ "url": "https://gitee.com/wxzzhj/covgate/issues"
48
+ },
49
+ "author": "wxzzhj"
50
+ }
@@ -0,0 +1,295 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://covgate.dev/schema/covgate.config.schema.json",
4
+ "title": "CovGate 配置",
5
+ "description": "CovGate 覆盖率门禁配置。客户端 scope 不得低于 100%,服务端 scope 不得低于 95%。",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": [
9
+ "scopes"
10
+ ],
11
+ "properties": {
12
+ "$schema": {
13
+ "type": "string"
14
+ },
15
+ "version": {
16
+ "const": 1,
17
+ "description": "配置格式版本,当前只支持 1。"
18
+ },
19
+ "scopes": {
20
+ "$ref": "#/$defs/scopes"
21
+ },
22
+ "waivers": {
23
+ "$ref": "#/$defs/waivers"
24
+ },
25
+ "reports": {
26
+ "$ref": "#/$defs/reports"
27
+ }
28
+ },
29
+ "$defs": {
30
+ "stringOrStrings": {
31
+ "oneOf": [
32
+ {
33
+ "type": "string",
34
+ "minLength": 1
35
+ },
36
+ {
37
+ "type": "array",
38
+ "minItems": 1,
39
+ "items": {
40
+ "type": "string",
41
+ "minLength": 1
42
+ }
43
+ }
44
+ ]
45
+ },
46
+ "scopes": {
47
+ "type": "object",
48
+ "minProperties": 1,
49
+ "propertyNames": {
50
+ "pattern": "^[A-Za-z0-9_-]+$"
51
+ },
52
+ "additionalProperties": {
53
+ "$ref": "#/$defs/scope"
54
+ }
55
+ },
56
+ "scope": {
57
+ "type": "object",
58
+ "additionalProperties": false,
59
+ "required": [
60
+ "commands",
61
+ "coverage",
62
+ "sources"
63
+ ],
64
+ "properties": {
65
+ "profile": {
66
+ "enum": [
67
+ "client",
68
+ "server"
69
+ ],
70
+ "description": "client 底线 100%;server 底线 95%。scope 名为 client/server 时可省略。"
71
+ },
72
+ "label": {
73
+ "type": "string",
74
+ "minLength": 1
75
+ },
76
+ "commands": {
77
+ "type": "array",
78
+ "minItems": 1,
79
+ "description": "按顺序执行的测试命令;任一命令失败则门禁失败。",
80
+ "items": {
81
+ "$ref": "#/$defs/command"
82
+ }
83
+ },
84
+ "coverage": {
85
+ "$ref": "#/$defs/coverage"
86
+ },
87
+ "thresholds": {
88
+ "type": "object",
89
+ "additionalProperties": false,
90
+ "description": "每个指标的门槛。只能高于产品底线,不能低于。",
91
+ "properties": {
92
+ "lines": {
93
+ "$ref": "#/$defs/threshold"
94
+ },
95
+ "statements": {
96
+ "$ref": "#/$defs/threshold"
97
+ },
98
+ "functions": {
99
+ "$ref": "#/$defs/threshold"
100
+ },
101
+ "branches": {
102
+ "$ref": "#/$defs/threshold"
103
+ }
104
+ }
105
+ },
106
+ "sources": {
107
+ "$ref": "#/$defs/sources"
108
+ },
109
+ "exclude": {
110
+ "type": "array",
111
+ "description": "文件级排除。每条都必须写 reason;没有匹配到任何源码文件的排除会失效并导致失败。",
112
+ "items": {
113
+ "$ref": "#/$defs/exclusion"
114
+ }
115
+ }
116
+ }
117
+ },
118
+ "command": {
119
+ "oneOf": [
120
+ {
121
+ "type": "string",
122
+ "minLength": 1
123
+ },
124
+ {
125
+ "type": "object",
126
+ "additionalProperties": false,
127
+ "required": [
128
+ "run"
129
+ ],
130
+ "properties": {
131
+ "name": {
132
+ "type": "string",
133
+ "minLength": 1
134
+ },
135
+ "run": {
136
+ "type": "string",
137
+ "minLength": 1
138
+ },
139
+ "cwd": {
140
+ "type": "string",
141
+ "minLength": 1,
142
+ "description": "相对配置文件所在目录。"
143
+ },
144
+ "timeoutSeconds": {
145
+ "type": "number",
146
+ "exclusiveMinimum": 0,
147
+ "maximum": 86400,
148
+ "default": 900
149
+ },
150
+ "env": {
151
+ "type": "object",
152
+ "additionalProperties": {
153
+ "type": [
154
+ "string",
155
+ "number",
156
+ "boolean"
157
+ ]
158
+ }
159
+ }
160
+ }
161
+ }
162
+ ]
163
+ },
164
+ "coverage": {
165
+ "type": "object",
166
+ "additionalProperties": false,
167
+ "required": [
168
+ "paths"
169
+ ],
170
+ "properties": {
171
+ "format": {
172
+ "enum": [
173
+ "auto",
174
+ "istanbul",
175
+ "lcov"
176
+ ],
177
+ "default": "auto",
178
+ "description": "auto 根据扩展名和内容推断;同一 scope 只能使用一种格式。"
179
+ },
180
+ "paths": {
181
+ "$ref": "#/$defs/stringOrStrings",
182
+ "description": "相对配置文件目录的覆盖率报告文件、目录或 glob。"
183
+ }
184
+ }
185
+ },
186
+ "sources": {
187
+ "type": "object",
188
+ "additionalProperties": false,
189
+ "required": [
190
+ "include"
191
+ ],
192
+ "properties": {
193
+ "include": {
194
+ "$ref": "#/$defs/stringOrStrings",
195
+ "description": "必须列出全部需要门禁的源码。未被测试加载、也不在报告中的源码会失败。"
196
+ }
197
+ }
198
+ },
199
+ "threshold": {
200
+ "type": "number",
201
+ "minimum": 0,
202
+ "maximum": 100,
203
+ "description": "百分比,最多两位小数;client 固定 100,server 最低 95。"
204
+ },
205
+ "exclusion": {
206
+ "type": "object",
207
+ "additionalProperties": false,
208
+ "required": [
209
+ "pattern",
210
+ "reason"
211
+ ],
212
+ "properties": {
213
+ "pattern": {
214
+ "type": "string",
215
+ "minLength": 1,
216
+ "description": "相对配置目录的 glob;没有斜杠时匹配任意层级同名文件。"
217
+ },
218
+ "reason": {
219
+ "type": "string",
220
+ "minLength": 1,
221
+ "description": "为什么这些文件不应参与覆盖率统计。占位词和过短说明会被拒绝。"
222
+ },
223
+ "owner": {
224
+ "type": "string",
225
+ "minLength": 1
226
+ },
227
+ "expires": {
228
+ "type": "string",
229
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
230
+ },
231
+ "ticket": {
232
+ "type": "string",
233
+ "minLength": 1
234
+ }
235
+ }
236
+ },
237
+ "waivers": {
238
+ "type": "object",
239
+ "additionalProperties": false,
240
+ "properties": {
241
+ "minReasonLength": {
242
+ "type": "integer",
243
+ "minimum": 1,
244
+ "maximum": 500,
245
+ "default": 8
246
+ },
247
+ "requireOwner": {
248
+ "type": "boolean",
249
+ "default": false
250
+ },
251
+ "stale": {
252
+ "enum": [
253
+ "fail",
254
+ "warn"
255
+ ],
256
+ "default": "fail",
257
+ "description": "豁免区间没有移除任何未覆盖代码时的处理方式。"
258
+ },
259
+ "expiry": {
260
+ "enum": [
261
+ "fail",
262
+ "warn"
263
+ ],
264
+ "default": "fail",
265
+ "description": "过期豁免的处理方式。"
266
+ }
267
+ }
268
+ },
269
+ "reports": {
270
+ "type": "object",
271
+ "additionalProperties": false,
272
+ "properties": {
273
+ "dir": {
274
+ "type": "string",
275
+ "minLength": 1,
276
+ "default": ".covgate"
277
+ },
278
+ "formats": {
279
+ "type": "array",
280
+ "uniqueItems": true,
281
+ "items": {
282
+ "enum": [
283
+ "json",
284
+ "markdown"
285
+ ]
286
+ },
287
+ "default": [
288
+ "json",
289
+ "markdown"
290
+ ]
291
+ }
292
+ }
293
+ }
294
+ }
295
+ }
@@ -0,0 +1,224 @@
1
+ import { validateReason } from "./reason.js";
2
+
3
+ const DIRECTIVE_PATTERNS = [
4
+ { kind: "next-line", regex: /\bcovgate-ignore-next-line\b\s*:\s*(.*)$/i },
5
+ { kind: "line", regex: /\bcovgate-ignore-line\b\s*:\s*(.*)$/i },
6
+ { kind: "start", regex: /\bcovgate-ignore-start\b\s*:\s*(.*)$/i },
7
+ { kind: "end", regex: /\bcovgate-ignore-end\b\s*:?\s*(.*)$/i },
8
+ { kind: "file", regex: /\bcovgate-ignore-file\b\s*:\s*(.*)$/i }
9
+ ];
10
+
11
+ const NATIVE_IGNORE = /\b(?:istanbul|c8|nyc|v8|node:coverage)\s+(?:ignore|disable)\b/i;
12
+ const ANY_DIRECTIVE = /\bcovgate-ignore\b/i;
13
+ const METADATA_KEYS = new Set(["owner", "expires", "ticket"]);
14
+
15
+ /**
16
+ * @param {string} text
17
+ */
18
+ function cleanCommentTail(text) {
19
+ return text
20
+ .replace(/\*\/\s*$/, "")
21
+ .replace(/-->\s*$/, "")
22
+ .replace(/#}\s*$/, "")
23
+ .trim();
24
+ }
25
+
26
+ /**
27
+ * @param {string} raw
28
+ * @param {number} lineNumber
29
+ * @returns {{ reason: string, metadata: Record<string, string>, errors: string[] }}
30
+ */
31
+ function parseReasonAndMetadata(raw, lineNumber) {
32
+ const errors = [];
33
+ const cleaned = cleanCommentTail(raw);
34
+ const segments = cleaned.split("|");
35
+ const reason = segments.shift()?.trim() ?? "";
36
+ /** @type {Record<string, string>} */
37
+ const metadata = {};
38
+ for (const segment of segments) {
39
+ const match = /^\s*([A-Za-z][A-Za-z0-9_-]*)\s*=\s*(.*?)\s*$/.exec(segment);
40
+ if (!match) {
41
+ errors.push(
42
+ "第 " +
43
+ lineNumber +
44
+ " 行:元数据格式错误,应为 owner=...、expires=YYYY-MM-DD 或 ticket=..."
45
+ );
46
+ continue;
47
+ }
48
+ const key = match[1].toLowerCase();
49
+ const value = match[2].trim();
50
+ if (!METADATA_KEYS.has(key)) {
51
+ errors.push('第 ' + lineNumber + ' 行:未知元数据 "' + key + '"');
52
+ continue;
53
+ }
54
+ if (!value) {
55
+ errors.push('第 ' + lineNumber + ' 行:元数据 "' + key + '" 不能为空');
56
+ continue;
57
+ }
58
+ if (metadata[key]) {
59
+ errors.push('第 ' + lineNumber + ' 行:元数据 "' + key + '" 重复');
60
+ continue;
61
+ }
62
+ metadata[key] = value;
63
+ }
64
+ return { reason, metadata, errors };
65
+ }
66
+
67
+ /**
68
+ * Parse covgate-ignore directives from source text. The parser is line-oriented
69
+ * so it works for JS/TS, Python, Go, Java and many other comment syntaxes.
70
+ * Directive text may appear in any comment.
71
+ *
72
+ * @param {string} text
73
+ * @param {string} relativePath
74
+ * @param {{ minReasonLength: number, requireOwner: boolean }} policy
75
+ * @param {Date} [now]
76
+ */
77
+ export function parseAnnotations(text, relativePath, policy, now = new Date()) {
78
+ const lines = text.replace(/^\uFEFF/, "").split(/\r?\n/);
79
+ /** @type {Array<any>} */
80
+ const directives = [];
81
+ /** @type {Array<{ line: number, text: string }>} */
82
+ const nativeIgnores = [];
83
+ /** @type {Array<{ line: number, message: string }>} */
84
+ const errors = [];
85
+ /** @type {{ line: number, directive: any } | null} */
86
+ let openBlock = null;
87
+
88
+ for (let index = 0; index < lines.length; index += 1) {
89
+ const lineNumber = index + 1;
90
+ const line = lines[index];
91
+ const nativeMatch = NATIVE_IGNORE.exec(line);
92
+ if (nativeMatch) {
93
+ nativeIgnores.push({ line: lineNumber, text: line.trim() });
94
+ }
95
+ let matched = null;
96
+ for (const pattern of DIRECTIVE_PATTERNS) {
97
+ const match = pattern.regex.exec(line);
98
+ if (match) {
99
+ matched = { kind: pattern.kind, raw: match[1] ?? "" };
100
+ break;
101
+ }
102
+ }
103
+ if (!matched) {
104
+ if (ANY_DIRECTIVE.test(line)) {
105
+ errors.push({
106
+ line: lineNumber,
107
+ message:
108
+ "covgate 指令格式错误;可用:covgate-ignore-line、next-line、start、end、file,且除 end 外都必须有冒号和原因"
109
+ });
110
+ }
111
+ continue;
112
+ }
113
+ if (matched.kind === "end") {
114
+ if (!openBlock) {
115
+ errors.push({
116
+ line: lineNumber,
117
+ message: "covgate-ignore-end 没有对应的 covgate-ignore-start"
118
+ });
119
+ continue;
120
+ }
121
+ const startDirective = openBlock.directive;
122
+ const endLine = lineNumber - 1;
123
+ const startLine = openBlock.line + 1;
124
+ if (startLine > endLine) {
125
+ errors.push({
126
+ line: lineNumber,
127
+ message: "covgate-ignore-start/end 之间没有代码行"
128
+ });
129
+ } else {
130
+ startDirective.startLine = startLine;
131
+ startDirective.endLine = endLine;
132
+ directives.push(startDirective);
133
+ }
134
+ openBlock = null;
135
+ continue;
136
+ }
137
+
138
+ const parsed = parseReasonAndMetadata(matched.raw, lineNumber);
139
+ errors.push(
140
+ ...parsed.errors.map((message) => ({ line: lineNumber, message }))
141
+ );
142
+ const validation = validateReason(
143
+ parsed.reason,
144
+ policy,
145
+ parsed.metadata,
146
+ now
147
+ );
148
+ for (const message of validation.errors) {
149
+ errors.push({ line: lineNumber, message });
150
+ }
151
+ const base = {
152
+ kind: matched.kind,
153
+ file: relativePath,
154
+ reason: parsed.reason,
155
+ owner: parsed.metadata.owner,
156
+ expires: parsed.metadata.expires,
157
+ ticket: parsed.metadata.ticket,
158
+ directiveLine: lineNumber,
159
+ expired: validation.expired,
160
+ validationErrors: validation.errors
161
+ };
162
+
163
+ if (matched.kind === "next-line") {
164
+ const target = lineNumber + 1;
165
+ if (target > lines.length) {
166
+ errors.push({
167
+ line: lineNumber,
168
+ message: "covgate-ignore-next-line 位于文件末尾,没有可忽略的下一行"
169
+ });
170
+ continue;
171
+ }
172
+ directives.push({ ...base, startLine: target, endLine: target });
173
+ } else if (matched.kind === "line") {
174
+ directives.push({ ...base, startLine: lineNumber, endLine: lineNumber });
175
+ } else if (matched.kind === "start") {
176
+ if (openBlock) {
177
+ errors.push({
178
+ line: lineNumber,
179
+ message: "covgate-ignore-start 不能嵌套;请先写 covgate-ignore-end"
180
+ });
181
+ continue;
182
+ }
183
+ openBlock = { line: lineNumber, directive: base };
184
+ } else if (matched.kind === "file") {
185
+ directives.push({
186
+ ...base,
187
+ startLine: 1,
188
+ endLine: lines.length,
189
+ fileLevel: true
190
+ });
191
+ }
192
+ }
193
+
194
+ if (openBlock) {
195
+ errors.push({
196
+ line: openBlock.line,
197
+ message: "covgate-ignore-start 缺少对应的 covgate-ignore-end"
198
+ });
199
+ }
200
+
201
+ const fileIgnores = directives.filter((directive) => directive.fileLevel);
202
+ if (fileIgnores.length > 1) {
203
+ errors.push({
204
+ line: fileIgnores[1].directiveLine,
205
+ message: "同一文件只能有一个 covgate-ignore-file"
206
+ });
207
+ }
208
+ if (fileIgnores.length > 0) {
209
+ const others = directives.filter((directive) => !directive.fileLevel);
210
+ if (others.length > 0) {
211
+ errors.push({
212
+ line: others[0].directiveLine,
213
+ message: "covgate-ignore-file 不能与行级豁免同时使用"
214
+ });
215
+ }
216
+ }
217
+
218
+ return {
219
+ directives: directives.filter((directive) => !directive.fileLevel),
220
+ fileIgnore: fileIgnores[0] ?? null,
221
+ nativeIgnores,
222
+ errors
223
+ };
224
+ }