covgate 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +502 -2
  3. package/bin/covgate-postinstall.js +13 -0
  4. package/bin/covgate.js +5 -0
  5. package/docs/ai-workflow.md +196 -0
  6. package/docs/design.md +327 -0
  7. package/docs/setup.md +163 -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 +327 -0
  33. package/src/setup/config.js +112 -0
  34. package/src/setup/index.js +242 -0
  35. package/src/setup/project.js +316 -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 +9 -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
@@ -0,0 +1,343 @@
1
+ import {
2
+ displayPercent,
3
+ escapeMarkdown,
4
+ formatDuration,
5
+ formatThreshold
6
+ } from "../util.js";
7
+
8
+ const METRICS = ["lines", "statements", "functions", "branches"];
9
+ const METRIC_LABELS = {
10
+ lines: "行",
11
+ statements: "语句",
12
+ functions: "函数",
13
+ branches: "分支"
14
+ };
15
+ const BT = String.fromCharCode(96);
16
+ const FENCE = BT.repeat(3);
17
+
18
+ /**
19
+ * @param {unknown} value
20
+ */
21
+ function code(value) {
22
+ return BT + String(value) + BT;
23
+ }
24
+
25
+ /**
26
+ * @param {string} value
27
+ */
28
+ function fence(value) {
29
+ let marker = FENCE;
30
+ while (value.includes(marker)) marker += BT;
31
+ return [marker + "text", value, marker];
32
+ }
33
+
34
+ /**
35
+ * @param {any} scope
36
+ */
37
+ function scopeTableRow(scope) {
38
+ const cells = [
39
+ escapeMarkdown(scope.label) + " (" + code(scope.name) + ")",
40
+ scope.profile
41
+ ];
42
+ for (const metric of METRICS) {
43
+ const value = scope.metrics[metric];
44
+ if (!value.applicable || value.percent === null) {
45
+ cells.push("N/A");
46
+ continue;
47
+ }
48
+ cells.push(
49
+ displayPercent(value.percent) +
50
+ " (" +
51
+ value.covered +
52
+ "/" +
53
+ value.total +
54
+ ") / " +
55
+ formatThreshold(value.threshold) +
56
+ "% " +
57
+ (value.passed ? "✓" : "✗")
58
+ );
59
+ }
60
+ cells.push(scope.passed ? "✓ 通过" : "✗ 未通过");
61
+ return "| " + cells.join(" | ") + " |";
62
+ }
63
+
64
+ /**
65
+ * @param {any} report
66
+ */
67
+ export function renderMarkdownReport(report) {
68
+ const lines = [];
69
+ const statusText =
70
+ report.status === "pass"
71
+ ? "通过"
72
+ : report.status === "fail"
73
+ ? "未通过"
74
+ : "运行失败";
75
+ lines.push("# CovGate 覆盖率报告");
76
+ lines.push("");
77
+ lines.push("- 状态:**" + statusText + "**");
78
+ lines.push("- 生成时间:" + report.generatedAt);
79
+ lines.push(
80
+ "- 模式:" + (report.mode === "check" ? "只检查已有报告" : "执行测试并检查")
81
+ );
82
+ lines.push(
83
+ "- 配置:" +
84
+ code(report.config.path) +
85
+ "(sha256: " +
86
+ code(report.config.sha256 ?? "n/a") +
87
+ ")"
88
+ );
89
+ lines.push("- 用时:" + formatDuration(report.durationMs));
90
+ lines.push("");
91
+
92
+ for (const issue of report.issues) {
93
+ lines.push(
94
+ "> " + (issue.severity === "error" ? "✗" : "!") + " " + issue.message
95
+ );
96
+ if (issue.outputTail) {
97
+ lines.push("");
98
+ lines.push(...fence(issue.outputTail));
99
+ }
100
+ }
101
+ if (report.issues.length > 0) lines.push("");
102
+
103
+ lines.push("## 总览");
104
+ lines.push("");
105
+ lines.push("| Scope | 类型 | 行 | 语句 | 函数 | 分支 | 结果 |");
106
+ lines.push("| --- | --- | --- | --- | --- | --- | --- |");
107
+ for (const scope of report.scopes) lines.push(scopeTableRow(scope));
108
+ lines.push("");
109
+ lines.push(
110
+ "总计:scope " +
111
+ report.summary.scopes +
112
+ " 个,通过 " +
113
+ report.summary.passedScopes +
114
+ " 个;豁免 " +
115
+ report.summary.waivers +
116
+ " 条;配置排除 " +
117
+ report.summary.exclusions +
118
+ " 条;缺失源码报告 " +
119
+ report.summary.missingSources +
120
+ " 个。"
121
+ );
122
+ lines.push("");
123
+
124
+ for (const scope of report.scopes) {
125
+ lines.push("## " + escapeMarkdown(scope.label) + "(" + scope.name + ")");
126
+ lines.push("");
127
+ lines.push("- 结果:**" + (scope.passed ? "通过" : "未通过") + "**");
128
+ lines.push("- 覆盖率格式:" + code(scope.format));
129
+ lines.push(
130
+ "- 源码文件:" +
131
+ scope.expectedSourceCount +
132
+ ";报告文件:" +
133
+ scope.reportedFileCount
134
+ );
135
+ lines.push(
136
+ "- 豁免:" +
137
+ scope.waivers.length +
138
+ ";配置排除:" +
139
+ scope.exclusions.length
140
+ );
141
+
142
+ if (scope.missingSources.length > 0) {
143
+ lines.push("");
144
+ lines.push("### 缺失覆盖率报告");
145
+ lines.push("");
146
+ for (const file of scope.missingSources) lines.push("- " + code(file));
147
+ }
148
+
149
+ const issues = scope.issues.concat(
150
+ scope.warnings.map((warning) => ({ ...warning, severity: "warning" }))
151
+ );
152
+ if (issues.length > 0) {
153
+ lines.push("");
154
+ lines.push("### 问题");
155
+ lines.push("");
156
+ for (const issue of issues) {
157
+ const location = issue.file ? " " + code(issue.file) : "";
158
+ lines.push(
159
+ "- **" + issue.severity + "**" + location + ": " + issue.message
160
+ );
161
+ }
162
+ lines.push("");
163
+ }
164
+
165
+ if (scope.failingFiles.length > 0) {
166
+ lines.push("");
167
+ lines.push("### 未覆盖明细");
168
+ lines.push("");
169
+ for (const file of scope.failingFiles) {
170
+ lines.push("#### " + code(file.path));
171
+ lines.push("");
172
+ lines.push(
173
+ "- 行:" +
174
+ displayPercent(file.metrics.lines?.percent ?? null) +
175
+ " (" +
176
+ (file.metrics.lines?.covered ?? 0) +
177
+ "/" +
178
+ (file.metrics.lines?.total ?? 0) +
179
+ ")"
180
+ );
181
+ if (file.metrics.statements) {
182
+ lines.push(
183
+ "- 语句:" +
184
+ displayPercent(file.metrics.statements.percent) +
185
+ " (" +
186
+ file.metrics.statements.covered +
187
+ "/" +
188
+ file.metrics.statements.total +
189
+ ")"
190
+ );
191
+ }
192
+ lines.push(
193
+ "- 函数:" +
194
+ displayPercent(file.metrics.functions.percent) +
195
+ " (" +
196
+ file.metrics.functions.covered +
197
+ "/" +
198
+ file.metrics.functions.total +
199
+ ")"
200
+ );
201
+ lines.push(
202
+ "- 分支:" +
203
+ displayPercent(file.metrics.branches.percent) +
204
+ " (" +
205
+ file.metrics.branches.covered +
206
+ "/" +
207
+ file.metrics.branches.total +
208
+ ")"
209
+ );
210
+ if (file.uncovered.linesTotal > 0) {
211
+ lines.push(
212
+ "- 未覆盖行:" + code(file.uncovered.lines.join(", "))
213
+ );
214
+ }
215
+ if (file.uncovered.statementsTotal > 0) {
216
+ lines.push(
217
+ "- 未覆盖语句行:" +
218
+ code(file.uncovered.statements.map((unit) => unit.line).join(", "))
219
+ );
220
+ }
221
+ if (file.uncovered.functionsTotal > 0) {
222
+ lines.push(
223
+ "- 未覆盖函数:" +
224
+ file.uncovered.functions
225
+ .map((unit) => code(unit.name) + " @L" + unit.line)
226
+ .join(", ")
227
+ );
228
+ }
229
+ if (file.uncovered.branchesTotal > 0) {
230
+ lines.push(
231
+ "- 未覆盖分支:" +
232
+ file.uncovered.branches
233
+ .map((unit) => "L" + unit.line + "#" + unit.index)
234
+ .join(", ")
235
+ );
236
+ }
237
+ lines.push("");
238
+ }
239
+ }
240
+
241
+ if (scope.waivers.length > 0) {
242
+ lines.push("### 豁免审计");
243
+ lines.push("");
244
+ lines.push("| 文件 | 行 | 状态 | 原因 | owner | expires | ticket |");
245
+ lines.push("| --- | --- | --- | --- | --- | --- | --- |");
246
+ for (const waiver of scope.waivers) {
247
+ const range =
248
+ waiver.kind === "file"
249
+ ? "整个文件"
250
+ : waiver.startLine + "-" + waiver.endLine;
251
+ lines.push(
252
+ "| " +
253
+ code(escapeMarkdown(waiver.file)) +
254
+ " | " +
255
+ range +
256
+ " | " +
257
+ waiver.status +
258
+ " | " +
259
+ escapeMarkdown(waiver.reason) +
260
+ " | " +
261
+ escapeMarkdown(waiver.owner ?? "") +
262
+ " | " +
263
+ (waiver.expires ?? "") +
264
+ " | " +
265
+ escapeMarkdown(waiver.ticket ?? "") +
266
+ " |"
267
+ );
268
+ }
269
+ lines.push("");
270
+ }
271
+
272
+ if (scope.exclusions.length > 0) {
273
+ lines.push("### 配置排除");
274
+ lines.push("");
275
+ lines.push("| 模式 | 文件数 | 状态 | 原因 | owner | expires |");
276
+ lines.push("| --- | --- | --- | --- | --- | --- |");
277
+ for (const exclusion of scope.exclusions) {
278
+ lines.push(
279
+ "| " +
280
+ code(escapeMarkdown(exclusion.pattern)) +
281
+ " | " +
282
+ exclusion.fileCount +
283
+ " | " +
284
+ exclusion.status +
285
+ " | " +
286
+ escapeMarkdown(exclusion.reason) +
287
+ " | " +
288
+ escapeMarkdown(exclusion.owner ?? "") +
289
+ " | " +
290
+ (exclusion.expires ?? "") +
291
+ " |"
292
+ );
293
+ }
294
+ lines.push("");
295
+ }
296
+ }
297
+
298
+ if (report.commands.length > 0) {
299
+ lines.push("## 测试命令");
300
+ lines.push("");
301
+ lines.push("| Scope | 命令 | 退出码 | 用时 |");
302
+ lines.push("| --- | --- | --- | --- |");
303
+ for (const command of report.commands) {
304
+ lines.push(
305
+ "| " +
306
+ command.scope +
307
+ " | " +
308
+ code(escapeMarkdown(command.command)) +
309
+ " | " +
310
+ (command.exitCode ?? "null") +
311
+ " | " +
312
+ formatDuration(command.durationMs) +
313
+ " |"
314
+ );
315
+ }
316
+ lines.push("");
317
+ }
318
+
319
+ const targetLabels = report.scopes
320
+ .map(
321
+ (scope) =>
322
+ scope.label +
323
+ ":" +
324
+ METRICS.filter((metric) => scope.metrics[metric].applicable)
325
+ .map(
326
+ (metric) =>
327
+ METRIC_LABELS[metric] +
328
+ "≥" +
329
+ formatThreshold(scope.metrics[metric].threshold) +
330
+ "%"
331
+ )
332
+ .join("、")
333
+ )
334
+ .filter(Boolean);
335
+ if (targetLabels.length > 0) {
336
+ lines.push("## 门槛");
337
+ lines.push("");
338
+ for (const label of targetLabels) lines.push("- " + label);
339
+ lines.push("");
340
+ }
341
+
342
+ return lines.join("\n") + "\n";
343
+ }
@@ -0,0 +1,267 @@
1
+ import {
2
+ displayPercent,
3
+ formatDuration,
4
+ formatThreshold
5
+ } from "../util.js";
6
+
7
+ const METRIC_LABELS = {
8
+ lines: "行",
9
+ statements: "语句",
10
+ functions: "函数",
11
+ branches: "分支"
12
+ };
13
+
14
+ const ESC = String.fromCharCode(27);
15
+
16
+ /**
17
+ * @param {any} report
18
+ * @param {{ color?: boolean }} [options]
19
+ */
20
+ export function renderTextReport(report, options = {}) {
21
+ const useColor = options.color === true && !process.env.NO_COLOR;
22
+ const paint = (code, value) =>
23
+ useColor ? ESC + "[" + code + "m" + value + ESC + "[0m" : value;
24
+ const green = (value) => paint("32", value);
25
+ const red = (value) => paint("31", value);
26
+ const yellow = (value) => paint("33", value);
27
+ const dim = (value) => paint("2", value);
28
+ const bold = (value) => paint("1", value);
29
+ const symbols = {
30
+ pass: green("✓"),
31
+ fail: red("✗"),
32
+ error: red("✗"),
33
+ warning: yellow("!")
34
+ };
35
+ const lines = [];
36
+
37
+ lines.push(bold("CovGate · 覆盖率门禁 v" + report.tool.version));
38
+ lines.push(
39
+ dim(
40
+ "模式 " +
41
+ (report.mode === "check" ? "只检查已有报告" : "执行测试并检查") +
42
+ " · 配置 " +
43
+ report.config.path +
44
+ " · 用时 " +
45
+ formatDuration(report.durationMs)
46
+ )
47
+ );
48
+ lines.push("");
49
+
50
+ for (const issue of report.issues) {
51
+ lines.push((symbols[issue.severity] ?? "?") + " " + issue.message);
52
+ if (issue.outputTail) lines.push(dim(issue.outputTail));
53
+ }
54
+ if (report.issues.length > 0) lines.push("");
55
+
56
+ for (const scope of report.scopes) {
57
+ const symbol = scope.passed ? symbols.pass : symbols.fail;
58
+ lines.push(
59
+ symbol +
60
+ " " +
61
+ bold(scope.label) +
62
+ " " +
63
+ dim("[" + scope.name + "/" + scope.profile + "]") +
64
+ " " +
65
+ (scope.passed ? green("通过") : red("未通过"))
66
+ );
67
+
68
+ const metricParts = [];
69
+ for (const metric of ["lines", "statements", "functions", "branches"]) {
70
+ const value = scope.metrics[metric];
71
+ if (!value.applicable || value.percent === null) {
72
+ metricParts.push(METRIC_LABELS[metric] + " N/A");
73
+ continue;
74
+ }
75
+ const state = value.passed ? "" : red(" ✗");
76
+ metricParts.push(
77
+ METRIC_LABELS[metric] +
78
+ " " +
79
+ displayPercent(value.percent) +
80
+ " " +
81
+ dim(
82
+ "(" +
83
+ value.covered +
84
+ "/" +
85
+ value.total +
86
+ ", 目标 " +
87
+ formatThreshold(value.threshold) +
88
+ "%)"
89
+ ) +
90
+ state
91
+ );
92
+ }
93
+ lines.push(" " + metricParts.join(" · "));
94
+ lines.push(
95
+ dim(
96
+ " 源码 " +
97
+ scope.expectedSourceCount +
98
+ " 个 · 报告 " +
99
+ scope.reportedFileCount +
100
+ " 个 · 豁免 " +
101
+ scope.waivers.length +
102
+ " 条 · 排除 " +
103
+ scope.exclusions.length +
104
+ " 条"
105
+ )
106
+ );
107
+
108
+ const errorIssues = scope.issues.filter((issue) => issue.severity === "error");
109
+ const warningIssues = scope.warnings.concat(
110
+ scope.issues.filter((issue) => issue.severity === "warning")
111
+ );
112
+ const shownIssues = errorIssues.concat(warningIssues).slice(0, 8);
113
+ if (shownIssues.length > 0) {
114
+ lines.push("");
115
+ for (const issue of shownIssues) {
116
+ const prefix = issue.severity === "error" ? red(" ✗") : yellow(" !");
117
+ const location = issue.file
118
+ ? " " + issue.file + (issue.line ? ":" + issue.line : "")
119
+ : "";
120
+ lines.push(prefix + location + " " + issue.message);
121
+ }
122
+ const rest = errorIssues.length + warningIssues.length - shownIssues.length;
123
+ if (rest > 0) lines.push(dim(" … 还有 " + rest + " 条问题,见报告文件"));
124
+ }
125
+
126
+ if (!scope.passed && scope.failingFiles.length > 0) {
127
+ lines.push("");
128
+ lines.push(" 未覆盖明细:");
129
+ for (const file of scope.failingFiles.slice(0, 12)) {
130
+ const percent = file.metrics.lines?.percent;
131
+ lines.push(
132
+ " " +
133
+ file.path +
134
+ (percent === null || percent === undefined
135
+ ? ""
136
+ : dim(" (行 " + displayPercent(percent) + ")"))
137
+ );
138
+ if (file.uncovered.linesTotal > 0) {
139
+ const values = file.uncovered.lines.slice(0, 20).join(", ");
140
+ const more =
141
+ file.uncovered.linesTotal > 20
142
+ ? " … + " + (file.uncovered.linesTotal - 20)
143
+ : "";
144
+ lines.push(" 行: " + values + more);
145
+ }
146
+ if ((file.uncovered.statementsTotal ?? 0) > 0) {
147
+ const values = file.uncovered.statements
148
+ .map((unit) => "L" + unit.line)
149
+ .slice(0, 15)
150
+ .join(", ");
151
+ const more =
152
+ (file.uncovered.statementsTotal ?? 0) > 15
153
+ ? " … + " + ((file.uncovered.statementsTotal ?? 0) - 15)
154
+ : "";
155
+ lines.push(" 语句: " + values + more);
156
+ }
157
+ if (file.uncovered.functionsTotal > 0) {
158
+ const values = file.uncovered.functions
159
+ .slice(0, 10)
160
+ .map((unit) => unit.name + "@L" + unit.line)
161
+ .join(", ");
162
+ const more =
163
+ file.uncovered.functionsTotal > 10
164
+ ? " … + " + (file.uncovered.functionsTotal - 10)
165
+ : "";
166
+ lines.push(" 函数: " + values + more);
167
+ }
168
+ if (file.uncovered.branchesTotal > 0) {
169
+ const values = file.uncovered.branches
170
+ .slice(0, 10)
171
+ .map((unit) => "L" + unit.line + "#" + unit.index)
172
+ .join(", ");
173
+ const more =
174
+ file.uncovered.branchesTotal > 10
175
+ ? " … + " + (file.uncovered.branchesTotal - 10)
176
+ : "";
177
+ lines.push(" 分支: " + values + more);
178
+ }
179
+ }
180
+ if (scope.failingFiles.length > 12) {
181
+ lines.push(
182
+ dim(" … 还有 " + (scope.failingFiles.length - 12) + " 个文件,见报告文件")
183
+ );
184
+ }
185
+ }
186
+
187
+ if (scope.exclusions.length > 0) {
188
+ lines.push("");
189
+ lines.push(" 配置排除(均需人工复核):");
190
+ for (const exclusion of scope.exclusions.slice(0, 10)) {
191
+ const status =
192
+ exclusion.status === "applied"
193
+ ? ""
194
+ : red(" [" + exclusion.status + "]");
195
+ lines.push(
196
+ " " +
197
+ exclusion.pattern +
198
+ " (" +
199
+ exclusion.fileCount +
200
+ " 个文件) — " +
201
+ exclusion.reason +
202
+ status
203
+ );
204
+ }
205
+ if (scope.exclusions.length > 10) {
206
+ lines.push(
207
+ dim(" … 还有 " + (scope.exclusions.length - 10) + " 条,见报告文件")
208
+ );
209
+ }
210
+ }
211
+
212
+ if (scope.waivers.length > 0) {
213
+ lines.push("");
214
+ lines.push(" 豁免(均需人工复核):");
215
+ for (const waiver of scope.waivers.slice(0, 10)) {
216
+ const location =
217
+ waiver.kind === "file"
218
+ ? waiver.file
219
+ : waiver.file +
220
+ ":" +
221
+ waiver.startLine +
222
+ (waiver.endLine !== waiver.startLine
223
+ ? "-" + waiver.endLine
224
+ : "");
225
+ const status =
226
+ waiver.status === "applied" ? "" : red(" [" + waiver.status + "]");
227
+ const meta = [
228
+ waiver.owner ? "owner=" + waiver.owner : null,
229
+ waiver.expires ? "expires=" + waiver.expires : null
230
+ ]
231
+ .filter(Boolean)
232
+ .join(", ");
233
+ lines.push(
234
+ " " +
235
+ location +
236
+ " — " +
237
+ waiver.reason +
238
+ (meta ? dim(" (" + meta + ")") : "") +
239
+ status
240
+ );
241
+ }
242
+ if (scope.waivers.length > 10) {
243
+ lines.push(dim(" … 还有 " + (scope.waivers.length - 10) + " 条,见报告文件"));
244
+ }
245
+ }
246
+ lines.push("");
247
+ }
248
+
249
+ const statusColor = report.status === "pass" ? green : red;
250
+ const statusText =
251
+ report.status === "pass"
252
+ ? "通过"
253
+ : report.status === "fail"
254
+ ? "未通过"
255
+ : "运行失败";
256
+ lines.push(bold("结果") + ": " + statusColor(statusText));
257
+ if (report.artifacts.json) {
258
+ lines.push(dim("JSON 报告: " + report.artifacts.json));
259
+ }
260
+ if (report.artifacts.markdown) {
261
+ lines.push(dim("Markdown 报告: " + report.artifacts.markdown));
262
+ }
263
+ if (report.status !== "pass" && report.summary.failedScopes.length > 0) {
264
+ lines.push(dim("失败 scope: " + report.summary.failedScopes.join(", ")));
265
+ }
266
+ return lines.join("\n") + "\n";
267
+ }