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/src/util.js ADDED
@@ -0,0 +1,163 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ export const PACKAGE_ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
7
+ export const IS_WINDOWS = process.platform === "win32";
8
+
9
+ /** @param {string} value */
10
+ export function toPosix(value) {
11
+ return value.replaceAll("\\", "/");
12
+ }
13
+
14
+ /** @param {unknown} value */
15
+ export function isPlainObject(value) {
16
+ return typeof value === "object" && value !== null && !Array.isArray(value);
17
+ }
18
+
19
+ /** @param {string} absPath */
20
+ export function pathKey(absPath) {
21
+ const normalized = path.normalize(absPath);
22
+ return IS_WINDOWS ? normalized.toLowerCase() : normalized;
23
+ }
24
+
25
+ /**
26
+ * Canonical identity for matching files across coverage tools, source globs,
27
+ * Windows drive-letter casing and mixed slash styles.
28
+ * @param {string} absPath
29
+ */
30
+ export function canonicalPath(absPath) {
31
+ return pathKey(path.resolve(absPath));
32
+ }
33
+
34
+ /**
35
+ * @param {string} absPath
36
+ * @param {string} root
37
+ */
38
+ export function relativeDisplay(absPath, root) {
39
+ return toPosix(path.relative(root, absPath));
40
+ }
41
+
42
+ /** @param {number} ms */
43
+ export function formatDuration(ms) {
44
+ if (ms < 1_000) return `${Math.max(0, Math.round(ms))}ms`;
45
+ if (ms < 60_000) return `${(ms / 1_000).toFixed(1)}s`;
46
+ const minutes = Math.floor(ms / 60_000);
47
+ const seconds = Math.round((ms % 60_000) / 1_000);
48
+ return `${minutes}m ${seconds}s`;
49
+ }
50
+
51
+ /**
52
+ * @param {number} covered
53
+ * @param {number} total
54
+ * @param {number} [fractionDigits]
55
+ */
56
+ export function percentOf(covered, total, fractionDigits = 2) {
57
+ if (total === 0) return null;
58
+ return Number(((covered / total) * 100).toFixed(fractionDigits));
59
+ }
60
+
61
+ /** @param {number | null} value */
62
+ export function displayPercent(value) {
63
+ return value === null || value === undefined ? "N/A" : `${value.toFixed(2)}%`;
64
+ }
65
+
66
+ /** @param {number} value */
67
+ export function formatThreshold(value) {
68
+ return Number.isInteger(value) ? String(value) : String(value);
69
+ }
70
+
71
+ /** @param {number} value @param {number} min @param {number} max */
72
+ export function clamp(value, min, max) {
73
+ return Math.min(max, Math.max(min, value));
74
+ }
75
+
76
+ /** @param {string} text @param {number} max */
77
+ export function tail(text, max = 20_000) {
78
+ if (text.length <= max) return text;
79
+ return `… 已截断 ${text.length - max} 个字符 …\n${text.slice(-max)}`;
80
+ }
81
+
82
+ /** @param {string} text */
83
+ export function sha256(text) {
84
+ return createHash("sha256").update(text).digest("hex");
85
+ }
86
+
87
+ /** @param {string} text */
88
+ export function escapeMarkdown(text) {
89
+ return text.replaceAll("|", "\\|").replaceAll("\n", "<br>");
90
+ }
91
+
92
+ /**
93
+ * Parse YYYY-MM-DD as an inclusive local end-of-day date.
94
+ * @param {string} value
95
+ * @returns {Date | null}
96
+ */
97
+ export function parseExpiryDate(value) {
98
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return null;
99
+ const [year, month, day] = value.split("-").map(Number);
100
+ const date = new Date(year, month - 1, day, 23, 59, 59, 999);
101
+ if (
102
+ date.getFullYear() !== year ||
103
+ date.getMonth() !== month - 1 ||
104
+ date.getDate() !== day
105
+ ) {
106
+ return null;
107
+ }
108
+ return date;
109
+ }
110
+
111
+ /** @param {Date} [now] */
112
+ export function todayIso(now = new Date()) {
113
+ const year = now.getFullYear();
114
+ const month = String(now.getMonth() + 1).padStart(2, "0");
115
+ const day = String(now.getDate()).padStart(2, "0");
116
+ return `${year}-${month}-${day}`;
117
+ }
118
+
119
+ /** @param {string} filePath */
120
+ export async function readJsonFile(filePath) {
121
+ const text = await readFile(filePath, "utf8");
122
+ try {
123
+ return JSON.parse(text);
124
+ } catch (error) {
125
+ throw new SyntaxError(`无法解析 JSON 文件 ${filePath}: ${error.message}`);
126
+ }
127
+ }
128
+
129
+ /** @param {string} filePath @param {string} content */
130
+ export async function writeTextFile(filePath, content) {
131
+ await mkdir(path.dirname(filePath), { recursive: true });
132
+ await writeFile(filePath, content, "utf8");
133
+ }
134
+
135
+ /** @param {string} text @param {number} max */
136
+ export function firstLine(text, max = 240) {
137
+ const line = text.split(/\r?\n/, 1)[0] ?? "";
138
+ return line.length > max ? `${line.slice(0, max)}…` : line;
139
+ }
140
+
141
+ /** @param {PropertyKey} key */
142
+ export function keyToString(key) {
143
+ return typeof key === "symbol" ? key.description ?? String(key) : String(key);
144
+ }
145
+
146
+ /**
147
+ * @template T
148
+ * @param {T[]} values
149
+ * @param {(value: T) => string} key
150
+ * @returns {T[]}
151
+ */
152
+ export function uniqueBy(values, key) {
153
+ const seen = new Set();
154
+ const result = [];
155
+ for (const value of values) {
156
+ const id = key(value);
157
+ if (!seen.has(id)) {
158
+ seen.add(id);
159
+ result.push(value);
160
+ }
161
+ }
162
+ return result;
163
+ }
package/src/version.js ADDED
@@ -0,0 +1,8 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ const pkg = JSON.parse(
4
+ readFileSync(new URL("../package.json", import.meta.url), "utf8")
5
+ );
6
+
7
+ export const VERSION = pkg.version;
8
+ export const TOOL_NAME = "covgate";
@@ -0,0 +1,10 @@
1
+ ## 覆盖率门禁(完成定义)
2
+
3
+ - 任何代码修改完成后,必须运行 `npx --no-install covgate run --json`;**退出码 0 才能结束任务**。
4
+ - 退出码 `1`:补测试或修实现;`2`:修配置;`3`:修测试命令/覆盖率报告。不要绕过。
5
+ - 禁止降低 `covgate.config.json` 中的门槛、扩大 exclude、删除豁免检查来让门禁通过。任何配置改动都必须由人类评审。
6
+ - **行为变更必须新增或更新覆盖该行为的真实测试**;不能把“门禁失败”当作完成理由,也不得用空报告、`--passWithNoTests`、ignore 或降低门槛绕过。
7
+ - 禁止 `istanbul ignore`、`c8 ignore`、`v8 ignore`、`node:coverage ignore`。
8
+ - 确实不可达的代码使用 `covgate-ignore-next-line`、`covgate-ignore-start/end` 或 `covgate-ignore-file`,并写具体原因、owner 和 expires。
9
+ - 新增或修改的源码必须出现在覆盖率报告中;`sources.include` 是范围事实来源。
10
+ - 交付时附上 `covgate run` 的结果摘要;不要只说“已写测试”。
@@ -0,0 +1,7 @@
1
+ #!/bin/sh
2
+ # Generic agent stop hook: runs the gate and blocks completion on failure.
3
+ # Put it in scripts/ and call it from your agent's Stop hook configuration.
4
+ set -eu
5
+ root="$(git rev-parse --show-toplevel)"
6
+ cd "$root"
7
+ exec npx --no-install covgate run --json
@@ -0,0 +1,98 @@
1
+ {
2
+ "$schema": "./node_modules/covgate/schema/covgate.schema.json",
3
+ "version": 1,
4
+ "scopes": {
5
+ "client": {
6
+ "profile": "client",
7
+ "label": "客户端",
8
+ "commands": [
9
+ {
10
+ "name": "客户端测试与覆盖率",
11
+ "run": "npm --prefix web run test -- --coverage",
12
+ "timeoutSeconds": 900
13
+ }
14
+ ],
15
+ "coverage": {
16
+ "format": "istanbul",
17
+ "paths": [
18
+ "web/coverage/coverage-final.json"
19
+ ]
20
+ },
21
+ "sources": {
22
+ "include": [
23
+ "web/src/**/*.{ts,tsx,js,jsx}"
24
+ ]
25
+ },
26
+ "exclude": [
27
+ {
28
+ "pattern": "web/src/**/*.test.{ts,tsx,js,jsx}",
29
+ "reason": "测试文件,不是产品运行时代码",
30
+ "owner": "web-platform"
31
+ },
32
+ {
33
+ "pattern": "web/src/**/__tests__/**",
34
+ "reason": "测试目录,不是产品运行时代码",
35
+ "owner": "web-platform"
36
+ },
37
+ {
38
+ "pattern": "web/src/**/*.d.ts",
39
+ "reason": "类型声明文件,没有运行时代码",
40
+ "owner": "web-platform"
41
+ },
42
+ {
43
+ "pattern": "web/src/generated/**",
44
+ "reason": "由 OpenAPI 生成,仓库不手工维护,生成器有独立测试",
45
+ "owner": "api-platform",
46
+ "expires": "2027-03-31"
47
+ }
48
+ ]
49
+ },
50
+ "server": {
51
+ "profile": "server",
52
+ "label": "服务端",
53
+ "commands": [
54
+ {
55
+ "name": "服务端测试与覆盖率",
56
+ "run": "npm --prefix api run test -- --coverage",
57
+ "timeoutSeconds": 1200
58
+ }
59
+ ],
60
+ "coverage": {
61
+ "format": "lcov",
62
+ "paths": [
63
+ "api/coverage/lcov.info"
64
+ ]
65
+ },
66
+ "sources": {
67
+ "include": [
68
+ "api/src/**/*.{ts,js}"
69
+ ]
70
+ },
71
+ "exclude": [
72
+ {
73
+ "pattern": "api/src/**/*.test.{ts,js}",
74
+ "reason": "测试文件,不是产品运行时代码",
75
+ "owner": "api-platform"
76
+ },
77
+ {
78
+ "pattern": "api/src/**/*.d.ts",
79
+ "reason": "类型声明文件,没有运行时代码",
80
+ "owner": "api-platform"
81
+ }
82
+ ]
83
+ }
84
+ },
85
+ "waivers": {
86
+ "minReasonLength": 8,
87
+ "requireOwner": true,
88
+ "stale": "fail",
89
+ "expiry": "fail"
90
+ },
91
+ "reports": {
92
+ "dir": ".covgate",
93
+ "formats": [
94
+ "json",
95
+ "markdown"
96
+ ]
97
+ }
98
+ }
@@ -0,0 +1,41 @@
1
+ name: coverage-gate
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches: [main]
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ concurrency:
12
+ group: coverage-${{ github.workflow }}-${{ github.ref }}
13
+ cancel-in-progress: true
14
+
15
+ jobs:
16
+ coverage:
17
+ runs-on: ubuntu-latest
18
+ timeout-minutes: 30
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+
22
+ - uses: actions/setup-node@v4
23
+ with:
24
+ node-version: 22
25
+ cache: npm
26
+
27
+ - name: Install dependencies
28
+ run: npm ci
29
+
30
+ # covgate 需要通过 file:、workspace 或私有 registry 安装。
31
+ # 如果使用 npm link / 本地路径,请改成 CI 可复现的安装方式。
32
+ - name: Run coverage gate
33
+ run: npx --no-install covgate run --json
34
+
35
+ - name: Upload audit report
36
+ if: always()
37
+ uses: actions/upload-artifact@v4
38
+ with:
39
+ name: covgate-report
40
+ path: .covgate/
41
+ if-no-files-found: ignore
@@ -0,0 +1,5 @@
1
+ # CovGate report artifacts
2
+ /.covgate/
3
+
4
+ # Typical local coverage output (root only; do not ignore src/coverage)
5
+ /coverage/
@@ -0,0 +1,10 @@
1
+ #!/bin/sh
2
+ # CovGate pre-commit hook (optional)
3
+ # 安装:
4
+ # cp templates/pre-commit .git/hooks/pre-commit
5
+ # chmod +x .git/hooks/pre-commit
6
+ # 该 hook 会跑完整测试套件;如果团队只在 CI 跑覆盖率,可以不启用。
7
+ set -eu
8
+ root="$(git rev-parse --show-toplevel)"
9
+ cd "$root"
10
+ exec npx --no-install covgate run