@luolingp/request-doctor 0.16.11 → 0.16.13

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/README.md CHANGED
@@ -182,4 +182,15 @@ DSH 升级后,旧插件可能只认 `0.2.1-rc.2`,而你现在是 `0.2.1-rc.3
182
182
 
183
183
  ## License
184
184
 
185
- MIT
185
+ MIT
186
+ ## 维护者检查
187
+
188
+ 仓库里自带三个脚本,改完东西依次跑:
189
+
190
+ ```bash
191
+ npm test # 单元与集成测试
192
+ npm run check:quality # 代码质量门禁(复杂度、重复率、注释密度、文件长度)
193
+ npm run check:docs # 文档与实现是否还对得上
194
+ ```
195
+
196
+ `check:docs` 会核对:需求条目是否都有覆盖行、报告引用的测试是否存在、设计说明的模块清单是否与 `lib/` 一致、六份文档版本号、质量报告数字、路由与命令是否写进文档。它随包发布,也可以在解压后的包里直接跑。
@@ -2,7 +2,7 @@
2
2
 
3
3
  **产品名称**:request-doctor(请求急救员)
4
4
  **产品标识**:request-doctor
5
- **版本**:0.16.11
5
+ **版本**:0.16.13
6
6
 
7
7
  ---
8
8
 
@@ -1,7 +1,7 @@
1
1
  # 软件设计说明
2
2
 
3
3
  **产品名称**:request-doctor(请求急救员)
4
- **版本**:0.16.11
4
+ **版本**:0.16.13
5
5
  **编制依据**:GB/T 8567-2006《计算机软件文档编制规范》6.2
6
6
 
7
7
  ---
@@ -1,7 +1,7 @@
1
1
  # 软件测试报告
2
2
 
3
3
  **产品名称**:request-doctor(请求急救员)
4
- **版本**:0.16.11
4
+ **版本**:0.16.13
5
5
  **编制依据**:GB/T 8567-2006《计算机软件文档编制规范》6.6;GB/T 8566-2007 验证与确认过程
6
6
 
7
7
  ---
@@ -1,7 +1,7 @@
1
1
  # 软件安装部署说明
2
2
 
3
3
  **产品名称**:request-doctor(请求急救员)
4
- **版本**:0.16.11
4
+ **版本**:0.16.13
5
5
  **编制依据**:GB/T 8567-2006《计算机软件文档编制规范》6.7
6
6
 
7
7
  ---
@@ -1,7 +1,7 @@
1
1
  # 软件用户手册
2
2
 
3
3
  **产品名称**:request-doctor(请求急救员)
4
- **版本**:0.16.11
4
+ **版本**:0.16.13
5
5
  **编制依据**:GB/T 8567-2006《计算机软件文档编制规范》6.5
6
6
 
7
7
  ---
@@ -1,7 +1,7 @@
1
1
  # 软件质量度量报告
2
2
 
3
3
  **产品名称**:request-doctor(请求急救员)
4
- **版本**:0.16.11
4
+ **版本**:0.16.13
5
5
  **度量标准**:GB/T 45717-2025《信息技术 软件测量 软件质量测量 自动化的源代码质量测度》
6
6
  **度量工具**:`tools/quality-measure.mjs`(本仓库自带,可复现)
7
7
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "standard": "GB/T 45717-2025 信息技术 软件测量 软件质量测量 自动化的源代码质量测度",
3
- "generatedAt": "2026-10-29T17:32:43.272Z",
3
+ "generatedAt": "2026-10-29T17:42:30.275Z",
4
4
  "scope": {
5
5
  "files": 41,
6
6
  "totalLines": 5538,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luolingp/request-doctor",
3
- "version": "0.16.11",
3
+ "version": "0.16.13",
4
4
  "description": "自动修复客户端引起的请求失败,并让版本不匹配的 DSH 插件能装上、能跑起来。",
5
5
  "keywords": [
6
6
  "dsh-plugin",
@@ -33,7 +33,8 @@
33
33
  "cordis.patch.yml",
34
34
  "locale",
35
35
  "icon.svg",
36
- "docs"
36
+ "docs",
37
+ "tools"
37
38
  ],
38
39
  "engines": {
39
40
  "node": ">=20"
@@ -50,5 +51,10 @@
50
51
  "@deepseek-ai/dsh-client-ui-plugin-manager"
51
52
  ]
52
53
  }
54
+ },
55
+ "scripts": {
56
+ "test": "node --test test/*.test.mjs",
57
+ "check:quality": "node tools/quality-measure.mjs --threshold",
58
+ "check:docs": "node tools/doc-check.mjs"
53
59
  }
54
- }
60
+ }
@@ -0,0 +1,131 @@
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ // 文档与实现对一遍。持续改动之后,文档最容易悄悄过期:
6
+ // 报告里写着不存在的用例、设计说明漏了新模块、质量报告的数字停在旧版本。
7
+ //
8
+ // 用法:node tools/doc-check.mjs [--json]
9
+ // 退出码非 0 表示有对不上的地方。
10
+
11
+ // 默认查脚本所在仓库;发布包被解到别处时用 --root 指定。
12
+ const rootArg = process.argv.indexOf('--root');
13
+ const root = rootArg >= 0 && process.argv[rootArg + 1] !== undefined
14
+ ? process.argv[rootArg + 1]
15
+ : join(fileURLToPath(import.meta.url), '..', '..');
16
+ const read = (path) => readFileSync(join(root, path), 'utf8');
17
+ const asJson = process.argv.includes('--json');
18
+
19
+ const problems = [];
20
+
21
+ // 1) 需求条目是否都在覆盖矩阵里出现。
22
+ const requirements = read('docs/01-软件需求规格说明.md');
23
+ const report = read('docs/03-软件测试报告.md');
24
+ const requirementIds = [...requirements.matchAll(/^- (FR-[\d.]+[a-z]?)/gm)].map((match) => match[1]);
25
+ for (const id of requirementIds.filter((value) => !report.includes(value))) {
26
+ problems.push(`需求 ${id} 在测试报告里没有对应的覆盖行`);
27
+ }
28
+
29
+ // 2) 报告里引用的测试文件是否真的存在;反过来,测试是否都被登记。
30
+ // 测试目录不随包发布:没有就跳过与用例相关的检查,而不是崩掉。
31
+ const testDir = join(root, 'test');
32
+ const testFiles = existsSync(testDir) ? readdirSync(testDir).filter((name) => name.endsWith('.test.mjs')) : [];
33
+ // 没有测试目录(发布包里就是这样)时,逐条比对没有意义。
34
+ if (testFiles.length > 0) {
35
+ const claimed = new Set([...report.matchAll(/`test\/([\w.-]+\.test\.mjs)`/g)].map((match) => match[1]));
36
+ for (const name of claimed) {
37
+ if (!testFiles.includes(name)) problems.push(`报告引用了不存在的测试文件 test/${name}`);
38
+ }
39
+ for (const name of testFiles.filter((value) => !claimed.has(value))) {
40
+ problems.push(`测试文件 test/${name} 未在报告中登记`);
41
+ }
42
+
43
+ }
44
+ // 3) 设计说明的模块清单与 lib 目录双向比对。
45
+ const design = read('docs/02-软件设计说明.md');
46
+ const documented = new Set([...design.matchAll(/^\| `((?:lib\/)?[\w.-]+\.js)`/gm)].map((match) => match[1]));
47
+ const realModules = [
48
+ ...readdirSync(join(root, 'lib')).filter((name) => name.endsWith('.js')).map((name) => `lib/${name}`),
49
+ 'index.js',
50
+ 'client.js',
51
+ ];
52
+ for (const name of realModules.filter((value) => !documented.has(value))) {
53
+ problems.push(`设计说明未登记模块 ${name}`);
54
+ }
55
+ for (const name of documented) {
56
+ if (!realModules.includes(name)) problems.push(`设计说明登记了不存在的模块 ${name}`);
57
+ }
58
+
59
+ // 4) 六份文档的版本号要与 package.json 一致。
60
+ const version = JSON.parse(read('package.json')).version;
61
+ for (const name of readdirSync(join(root, 'docs')).filter((file) => file.endsWith('.md'))) {
62
+ const found = /\*\*版本\*\*:([\d.]+)/.exec(read(`docs/${name}`))?.[1];
63
+ if (found !== undefined && found !== version) problems.push(`docs/${name} 版本 ${found},应为 ${version}`);
64
+ }
65
+
66
+ // 5) 质量报告的实测表要与机器可读结果一致。
67
+ //
68
+ // 报告里有两张表都用「注释密度」这类标签:前面一张是定义(第三列是文字),
69
+ // 后面一张才是实测。所以先把实测表切出来,再在表内按行比对,避免匹配到定义表。
70
+ const measured = JSON.parse(read('docs/quality-report.json'));
71
+ const qualityDoc = read('docs/06-软件质量度量报告.md');
72
+ const tableStart = qualityDoc.indexOf('| 度量项 | 实测值 | 限值 | 判定 |');
73
+ const measuredTable = tableStart < 0 ? '' : qualityDoc.slice(tableStart).split(/\r?\n\r?\n/)[0];
74
+
75
+ const expected = [
76
+ ['文件数', String(measured.scope.files)],
77
+ ['总行数', String(measured.scope.totalLines)],
78
+ ['源代码行数', String(measured.scope.sourceLines)],
79
+ ['注释行数', String(measured.scope.commentLines)],
80
+ ['注释密度', `${measured.measures.commentDensity}%`],
81
+ ['重复率', `${measured.measures.duplicationPercent}%`],
82
+ ['函数数', String(measured.measures.functionCount)],
83
+ ['最大圈复杂度', String(measured.measures.maxComplexity)],
84
+ ['最长函数', `${measured.measures.maxFunctionLines} 行`],
85
+ ['最长文件', `${measured.measures.maxFileLines} 行`],
86
+ ];
87
+
88
+ if (measuredTable === '') {
89
+ problems.push('质量报告里找不到实测表');
90
+ } else {
91
+ for (const [label, value] of expected) {
92
+ const row = measuredTable
93
+ .split(/\r?\n/)
94
+ .find((line) => line.replace(/\*/g, '').startsWith(`| ${label} |`));
95
+ if (row === undefined) {
96
+ problems.push(`质量报告实测表缺少「${label}」`);
97
+ continue;
98
+ }
99
+ const cell = row.split('|')[2].replace(/\*/g, '').trim();
100
+ if (cell !== value) problems.push(`质量报告「${label}」为 ${cell},实测 ${value}`);
101
+ }
102
+ }
103
+
104
+ // 6) 每条界面路由和每个命令都要写进文档。
105
+ const manual = read('docs/05-用户手册.md');
106
+ const sources = [
107
+ read('index.js'),
108
+ read('client.js'),
109
+ ...realModules.filter((name) => name.startsWith('lib/')).map((name) => read(name)),
110
+ ].join('\n');
111
+
112
+ const routes = new Set([...sources.matchAll(/'(?:\/doctor\/[a-z]+)'/g)].map((match) => match[0].slice(1, -1)));
113
+ for (const route of routes) {
114
+ if (!manual.includes(route) && !design.includes(route)) problems.push(`路由 ${route} 未写进文档`);
115
+ }
116
+ for (const name of new Set([...sources.matchAll(/sub === '(\w+)'/g)].map((match) => match[1]))) {
117
+ if (!manual.includes(`/doctor ${name}`)) problems.push(`命令 /doctor ${name} 未写进用户手册`);
118
+ }
119
+
120
+ if (asJson) {
121
+ console.log(JSON.stringify({ ok: problems.length === 0, problems }, null, 2));
122
+ } else if (problems.length === 0) {
123
+ console.log(
124
+ `文档与实现一致:需求 ${requirementIds.length} 条、测试 ${testFiles.length} 个、模块 ${realModules.length} 个、版本 ${version}`,
125
+ );
126
+ } else {
127
+ for (const problem of problems) console.log(`- ${problem}`);
128
+ console.log(`\n共 ${problems.length} 处不一致`);
129
+ }
130
+
131
+ process.exit(problems.length === 0 ? 0 : 1);
@@ -0,0 +1,257 @@
1
+ // 源代码质量度量,对应 GB/T 45717-2025。
2
+ //
3
+ // 只算不需要执行代码就能得到的指标:规模、注释密度、圈复杂度、重复率、函数与文件长度。
4
+ // 带 --threshold 时任一项超限就以非零码退出,可以当门禁。
5
+ import { readFileSync, readdirSync, writeFileSync } from 'node:fs';
6
+ import { dirname, extname, join, relative, resolve } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
10
+
11
+ // 限值:函数超过十来个分支就很难穷举测试,注释不到源码一成说明意图没写清,
12
+ // 重复率超过百分之五基本是复制粘贴而不是抽象。
13
+ const LIMITS = {
14
+ maxComplexity: 10,
15
+ maxFunctionLines: 60,
16
+ maxFileLines: 500,
17
+ minCommentDensity: 10,
18
+ maxDuplicationPercent: 5,
19
+ };
20
+
21
+ // 只看产品代码。
22
+ function collectSources(root) {
23
+ const files = [];
24
+ const walk = (dir) => {
25
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
26
+ if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
27
+ const path = join(dir, entry.name);
28
+ if (entry.isDirectory()) {
29
+ if (['test', 'tools', 'docs'].includes(entry.name)) continue;
30
+ walk(path);
31
+ continue;
32
+ }
33
+ if (extname(entry.name) === '.js' || extname(entry.name) === '.mjs') files.push(path);
34
+ }
35
+ };
36
+ walk(root);
37
+ return files;
38
+ }
39
+
40
+ // 字符串字面量换成占位,免得里面的内容被当成代码。
41
+ function stripLiterals(line) {
42
+ return line
43
+ .replace(/`(?:\\.|[^`\\])*`/g, '``')
44
+ .replace(/'(?:\\.|[^'\\])*'/g, "''")
45
+ .replace(/"(?:\\.|[^"\\])*"/g, '""');
46
+ }
47
+
48
+ // 圈复杂度。一个 switch 的所有分支只算一次判断——那是查表,不是判定树。
49
+ function cyclomatic(block) {
50
+ const text = block.split('\n').map((line) => stripLiterals(line).replace(/\/\/.*$/, '')).join('\n');
51
+ const switches = (text.match(/\bswitch\s*\(/g) ?? []).length;
52
+ const cases = (text.match(/\bcase\s+/g) ?? []).length;
53
+ const patterns = [/\bif\s*\(/g, /\bfor\s*\(/g, /\bwhile\s*\(/g, /\bcatch\s*\(/g, /\?\?/g, /\?\./g, /&&/g, /\|\|/g];
54
+ let count = 1 + switches;
55
+ for (const pattern of patterns) count += (text.match(pattern) ?? []).length;
56
+ count += Math.max(0, cases - switches);
57
+ return count;
58
+ }
59
+
60
+ // 找出函数声明和它们的花括号范围。
61
+ function findFunctions(lines) {
62
+ const found = [];
63
+ const isDeclaration = (line) =>
64
+ /^\s*(?:export\s+)?(?:async\s+)?function\s+[A-Za-z_$][\w$]*\s*\(/.test(line)
65
+ || /^\s*(?:export\s+)?const\s+[A-Za-z_$][\w$]*\s*=\s*(?:async\s*)?\([^)]*\)\s*=>\s*\{/.test(line)
66
+ || /^\s*[A-Za-z_$][\w$]*\s*\([^)]*\)\s*\{/.test(line);
67
+
68
+ for (let index = 0; index < lines.length; index += 1) {
69
+ if (!isDeclaration(lines[index])) continue;
70
+ let depth = 0;
71
+ let started = false;
72
+ for (let scan = index; scan < lines.length; scan += 1) {
73
+ const code = stripLiterals(lines[scan]).replace(/\/\/.*$/, '');
74
+ for (const char of code) {
75
+ if (char === '{') {
76
+ depth += 1;
77
+ started = true;
78
+ } else if (char === '}') {
79
+ depth -= 1;
80
+ }
81
+ }
82
+ if (started && depth === 0) {
83
+ found.push({ startLine: index + 1, endLine: scan + 1, body: lines.slice(index, scan + 1) });
84
+ break;
85
+ }
86
+ }
87
+ }
88
+ return found;
89
+ }
90
+
91
+ // 去掉嵌套函数的行:外层函数的编排逻辑不该为内层的工作背复杂度。
92
+ function ownLines(fn, all) {
93
+ const nested = all.filter((other) => other !== fn && other.startLine > fn.startLine && other.endLine < fn.endLine);
94
+ if (nested.length === 0) return fn.body;
95
+ const covered = new Set();
96
+ for (const other of nested) {
97
+ for (let line = other.startLine; line <= other.endLine; line += 1) covered.add(line);
98
+ }
99
+ return fn.body.filter((_, offset) => {
100
+ const line = fn.startLine + offset;
101
+ return line === fn.startLine || line === fn.endLine || !covered.has(line);
102
+ });
103
+ }
104
+
105
+ // 量一个文件。
106
+ function measureFile(path) {
107
+ const lines = readFileSync(path, 'utf8').split('\n');
108
+ let commentLines = 0;
109
+ let blankLines = 0;
110
+ let inBlock = false;
111
+ for (const raw of lines) {
112
+ const line = raw.trim();
113
+ if (inBlock) {
114
+ commentLines += 1;
115
+ if (line.includes('*/')) inBlock = false;
116
+ continue;
117
+ }
118
+ if (line.startsWith('/*')) {
119
+ commentLines += 1;
120
+ if (!line.includes('*/')) inBlock = true;
121
+ continue;
122
+ }
123
+ if (line.startsWith('//')) {
124
+ commentLines += 1;
125
+ continue;
126
+ }
127
+ if (line === '') blankLines += 1;
128
+ }
129
+ const functions = findFunctions(lines);
130
+ return {
131
+ path: relative(ROOT, path),
132
+ totalLines: lines.length,
133
+ sourceLines: lines.length - blankLines - commentLines,
134
+ commentLines,
135
+ functions: functions.map((fn) => {
136
+ const own = ownLines(fn, functions);
137
+ return { startLine: fn.startLine, lines: own.length, complexity: cyclomatic([fn.body[0], ...own].join('\n')) };
138
+ }),
139
+ };
140
+ }
141
+
142
+ // 归一化一行,用于比较重复。
143
+ function normalize(line) {
144
+ return stripLiterals(line).replace(/\/\/.*$/, '').replace(/\s+/g, ' ').trim();
145
+ }
146
+
147
+ // 一行算不算「逻辑行」。注释、导入、纯标点、对象字段和正则遍历惯用法都不算:
148
+ // 它们重复是因为语言和数据结构本来如此,不是因为有人复制粘贴。
149
+ function isLogicLine(line) {
150
+ if (line.length < 30) return false;
151
+ if (line.startsWith('*') || line.startsWith('/*') || line.startsWith('//')) return false;
152
+ if (line.startsWith('import ') || line.startsWith('export {') || line.startsWith('} from')) return false;
153
+ if (/^[)}\];,]+$/.test(line)) return false;
154
+ if (/^[A-Za-z_$][\w$]*:\s*[^(){}]*,$/.test(line)) return false;
155
+ if (/^while \(\(match = /.test(line)) return false;
156
+ return true;
157
+ }
158
+
159
+ // 重复率。
160
+ function duplication(files, contents) {
161
+ const seen = new Map();
162
+ let total = 0;
163
+ let repeated = 0;
164
+ for (const file of files) {
165
+ for (const raw of contents.get(file).split('\n')) {
166
+ const line = normalize(raw);
167
+ if (!isLogicLine(line)) continue;
168
+ total += 1;
169
+ const count = seen.get(line) ?? 0;
170
+ seen.set(line, count + 1);
171
+ if (count > 0) repeated += 1;
172
+ }
173
+ }
174
+ return total === 0 ? 0 : (repeated / total) * 100;
175
+ }
176
+
177
+ const files = collectSources(ROOT);
178
+ const contents = new Map(files.map((file) => [file, readFileSync(file, 'utf8')]));
179
+ const measured = files.map(measureFile);
180
+ const duplicatePercent = duplication(files, contents);
181
+
182
+ const allFunctions = measured.flatMap((file) => file.functions.map((fn) => ({ file: file.path, ...fn })));
183
+ const sourceLines = measured.reduce((sum, file) => sum + file.sourceLines, 0);
184
+ const commentLines = measured.reduce((sum, file) => sum + file.commentLines, 0);
185
+ const density = sourceLines === 0 ? 100 : (commentLines / sourceLines) * 100;
186
+
187
+ const violations = [];
188
+ for (const fn of allFunctions) {
189
+ if (fn.complexity > LIMITS.maxComplexity) {
190
+ violations.push({ measure: '圈复杂度', file: fn.file, line: fn.startLine, value: fn.complexity, limit: LIMITS.maxComplexity });
191
+ }
192
+ if (fn.lines > LIMITS.maxFunctionLines) {
193
+ violations.push({ measure: '函数长度', file: fn.file, line: fn.startLine, value: fn.lines, limit: LIMITS.maxFunctionLines });
194
+ }
195
+ }
196
+ for (const file of measured) {
197
+ if (file.totalLines > LIMITS.maxFileLines) {
198
+ violations.push({ measure: '文件长度', file: file.path, line: 1, value: file.totalLines, limit: LIMITS.maxFileLines });
199
+ }
200
+ }
201
+ if (density < LIMITS.minCommentDensity) {
202
+ violations.push({ measure: '注释密度', file: '(总体)', line: 0, value: Number(density.toFixed(2)), limit: LIMITS.minCommentDensity });
203
+ }
204
+ if (duplicatePercent > LIMITS.maxDuplicationPercent) {
205
+ violations.push({ measure: '重复率', file: '(总体)', line: 0, value: Number(duplicatePercent.toFixed(2)), limit: LIMITS.maxDuplicationPercent });
206
+ }
207
+
208
+ const report = {
209
+ standard: 'GB/T 45717-2025 信息技术 软件测量 软件质量测量 自动化的源代码质量测度',
210
+ generatedAt: new Date().toISOString(),
211
+ scope: { files: files.length, totalLines: measured.reduce((sum, f) => sum + f.totalLines, 0), sourceLines, commentLines },
212
+ measures: {
213
+ commentDensity: Number(density.toFixed(2)),
214
+ duplicationPercent: Number(duplicatePercent.toFixed(2)),
215
+ functionCount: allFunctions.length,
216
+ maxComplexity: allFunctions.reduce((max, fn) => Math.max(max, fn.complexity), 0),
217
+ maxFunctionLines: allFunctions.reduce((max, fn) => Math.max(max, fn.lines), 0),
218
+ maxFileLines: measured.reduce((max, f) => Math.max(max, f.totalLines), 0),
219
+ averageComplexity: allFunctions.length === 0 ? 0 : Number((allFunctions.reduce((s, fn) => s + fn.complexity, 0) / allFunctions.length).toFixed(2)),
220
+ },
221
+ limits: LIMITS,
222
+ violations,
223
+ };
224
+
225
+ // --out 让工具自己写文件:用 shell 重定向容易被写成非 UTF-8,中文会变成乱码。
226
+ const outIndex = process.argv.indexOf('--out');
227
+ if (outIndex > 0 && process.argv[outIndex + 1] !== undefined) {
228
+ const json = JSON.stringify(report, null, 2) + String.fromCharCode(10);
229
+ writeFileSync(process.argv[outIndex + 1], json, 'utf8');
230
+ console.log('已写入 ' + process.argv[outIndex + 1]);
231
+ } else if (process.argv.includes('--json')) {
232
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
233
+ } else {
234
+ const rows = [
235
+ ['度量项', '实测值', '限值'],
236
+ ['文件数', String(files.length), '—'],
237
+ ['总行数', String(report.scope.totalLines), '—'],
238
+ ['源代码行数', String(sourceLines), '—'],
239
+ ['注释行数', String(commentLines), '—'],
240
+ ['注释密度(%)', String(report.measures.commentDensity), `≥ ${LIMITS.minCommentDensity}`],
241
+ ['重复率(%)', String(report.measures.duplicationPercent), `≤ ${LIMITS.maxDuplicationPercent}`],
242
+ ['函数数', String(report.measures.functionCount), '—'],
243
+ ['平均圈复杂度', String(report.measures.averageComplexity), '—'],
244
+ ['最大圈复杂度', String(report.measures.maxComplexity), `≤ ${LIMITS.maxComplexity}`],
245
+ ['最长函数(行)', String(report.measures.maxFunctionLines), `≤ ${LIMITS.maxFunctionLines}`],
246
+ ['最长文件(行)', String(report.measures.maxFileLines), `≤ ${LIMITS.maxFileLines}`],
247
+ ];
248
+ for (const row of rows) console.log(row.join('\t'));
249
+ console.log('');
250
+ if (violations.length === 0) console.log('结论:全部度量项满足限值。');
251
+ else {
252
+ console.log(`结论:${violations.length} 项超限。`);
253
+ for (const v of violations) console.log(` - ${v.measure} ${v.file}:${v.line} 实测 ${v.value},限值 ${v.limit}`);
254
+ }
255
+ }
256
+
257
+ if (process.argv.includes('--threshold') && violations.length > 0) process.exit(1);