code-abyss 1.7.0 → 1.7.2
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 +8 -6
- package/bin/install.js +59 -163
- package/bin/lib/ccline.js +82 -0
- package/bin/lib/utils.js +61 -0
- package/package.json +5 -2
- package/skills/domains/ai/SKILL.md +2 -0
- package/skills/domains/architecture/SKILL.md +2 -0
- package/skills/domains/data-engineering/SKILL.md +2 -0
- package/skills/domains/development/SKILL.md +2 -0
- package/skills/domains/devops/SKILL.md +2 -0
- package/skills/domains/frontend-design/SKILL.md +1 -0
- package/skills/domains/frontend-design/claymorphism/SKILL.md +2 -0
- package/skills/domains/frontend-design/glassmorphism/SKILL.md +2 -0
- package/skills/domains/frontend-design/liquid-glass/SKILL.md +2 -0
- package/skills/domains/frontend-design/neubrutalism/SKILL.md +2 -0
- package/skills/domains/infrastructure/SKILL.md +2 -0
- package/skills/domains/mobile/SKILL.md +2 -0
- package/skills/domains/orchestration/SKILL.md +2 -1
- package/skills/domains/security/SKILL.md +2 -0
- package/skills/orchestration/multi-agent/SKILL.md +2 -0
- package/skills/run_skill.js +14 -9
- package/skills/tools/gen-docs/scripts/doc_generator.js +21 -7
- package/skills/tools/lib/shared.js +98 -0
- package/skills/tools/verify-change/scripts/change_analyzer.js +73 -54
- package/skills/tools/verify-module/scripts/module_scanner.js +63 -37
- package/skills/tools/verify-quality/scripts/quality_checker.js +134 -73
- package/skills/tools/verify-security/scripts/security_scanner.js +212 -62
package/skills/run_skill.js
CHANGED
|
@@ -66,19 +66,24 @@ function acquireTargetLock(args) {
|
|
|
66
66
|
} catch (e) {
|
|
67
67
|
if (e.code === 'EEXIST') {
|
|
68
68
|
console.log(`⏳ 等待锁释放: ${target}`);
|
|
69
|
-
//
|
|
69
|
+
// 异步轮询等待,最多 30s
|
|
70
70
|
const deadline = Date.now() + 30000;
|
|
71
|
-
|
|
71
|
+
const poll = () => {
|
|
72
|
+
if (Date.now() >= deadline) { console.error(`⏳ 等待锁超时: ${target}`); process.exit(1); }
|
|
72
73
|
try {
|
|
73
74
|
const fd = openSync(lockPath, 'wx');
|
|
74
75
|
return { fd, lockPath };
|
|
75
|
-
} catch { /*
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
76
|
+
} catch { /* still locked */ }
|
|
77
|
+
return null;
|
|
78
|
+
};
|
|
79
|
+
const result = poll();
|
|
80
|
+
if (result) return result;
|
|
81
|
+
return new Promise((resolve) => {
|
|
82
|
+
const timer = setInterval(() => {
|
|
83
|
+
const r = poll();
|
|
84
|
+
if (r) { clearInterval(timer); resolve(r); }
|
|
85
|
+
}, 200);
|
|
86
|
+
});
|
|
82
87
|
}
|
|
83
88
|
// 其他错误,忽略锁继续执行
|
|
84
89
|
return { fd: null, lockPath: null };
|
|
@@ -103,7 +103,8 @@ const LANG_PATTERNS = {
|
|
|
103
103
|
'Rust': [/^\s*(?:pub\s+)?fn\s+(\w+)/, /^\s*(?:pub\s+)?struct\s+(\w+)/],
|
|
104
104
|
'TypeScript': [/^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)/, /^\s*(?:export\s+)?class\s+(\w+)/],
|
|
105
105
|
'JavaScript': [/^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)/, /^\s*(?:export\s+)?class\s+(\w+)/],
|
|
106
|
-
'Java': [/^\s*(?:public|private|protected)?\s*(?:static\s+)?\w+\s+(\w+)\s*\(/,
|
|
106
|
+
'Java': [/^\s*(?:public|private|protected)?\s*(?:static\s+)?\w+\s+(\w+)\s*\(/,
|
|
107
|
+
/^\s*(?:public\s+)?class\s+(\w+)/],
|
|
107
108
|
'C++': [/^\s*(?:\w+\s+)+(\w+)\s*\([^;]*$/, /^\s*class\s+(\w+)/],
|
|
108
109
|
'C': [/^\s*(?:\w+\s+)+(\w+)\s*\([^;]*$/, null],
|
|
109
110
|
};
|
|
@@ -176,18 +177,31 @@ function generateReadme(info) {
|
|
|
176
177
|
L.push('## 使用方法', '');
|
|
177
178
|
if (info.entry_points.length) {
|
|
178
179
|
L.push('### 运行', '', '```bash');
|
|
179
|
-
const cmds = {
|
|
180
|
+
const cmds = {
|
|
181
|
+
Python: `python -m ${info.name}`, Go: 'go run ./cmd/main.go',
|
|
182
|
+
Rust: 'cargo run', TypeScript: 'npm start', JavaScript: 'npm start'
|
|
183
|
+
};
|
|
180
184
|
L.push(cmds[info.language] || `# 请根据 ${info.language} 项目结构添加运行命令`);
|
|
181
185
|
L.push('```', '');
|
|
182
186
|
}
|
|
183
187
|
|
|
184
188
|
L.push('### 示例', '');
|
|
185
189
|
const EXAMPLES = {
|
|
186
|
-
Python: `from ${info.name.toLowerCase()} import main\n\n
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
190
|
+
Python: `from ${info.name.toLowerCase()} import main\n\n` +
|
|
191
|
+
`# 初始化\nobj = main()\n\n# 执行操作\nresult = obj.process()\nprint(result)`,
|
|
192
|
+
Go: `package main\n\nimport "${info.name.toLowerCase()}"\n\nfunc main() {\n` +
|
|
193
|
+
` // 初始化\n obj := ${info.name.toLowerCase()}.New()\n` +
|
|
194
|
+
`\n // 执行操作\n result := obj.Process()\n println(result)\n}`,
|
|
195
|
+
Rust: `use ${info.name.toLowerCase()}::*;\n\nfn main() {\n` +
|
|
196
|
+
` // 初始化\n let obj = Object::new();\n\n` +
|
|
197
|
+
` // 执行操作\n let result = obj.process();\n` +
|
|
198
|
+
` println!("{}", result);\n}`,
|
|
199
|
+
TypeScript: `import { main } from "./${info.name.toLowerCase()}";\n\n` +
|
|
200
|
+
`// 初始化\nconst obj = new main();\n\n` +
|
|
201
|
+
`// 执行操作\nconst result = obj.process();\nconsole.log(result);`,
|
|
202
|
+
JavaScript: `const { main } = require("./${info.name.toLowerCase()}");\n\n` +
|
|
203
|
+
`// 初始化\nconst obj = new main();\n\n` +
|
|
204
|
+
`// 执行操作\nconst result = obj.process();\nconsole.log(result);`,
|
|
191
205
|
};
|
|
192
206
|
if (EXAMPLES[info.language]) {
|
|
193
207
|
L.push('```' + info.language.toLowerCase(), EXAMPLES[info.language], '```');
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 验证工具共享库
|
|
5
|
+
* 消灭 verify-* 脚本间的重复代码
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// --- CLI 参数解析 ---
|
|
9
|
+
|
|
10
|
+
function parseCliArgs(argv, extraFlags) {
|
|
11
|
+
const args = argv.slice(2);
|
|
12
|
+
const result = { target: '.', verbose: false, json: false };
|
|
13
|
+
if (extraFlags) Object.assign(result, extraFlags);
|
|
14
|
+
|
|
15
|
+
for (let i = 0; i < args.length; i++) {
|
|
16
|
+
if (args[i] === '-v' || args[i] === '--verbose') result.verbose = true;
|
|
17
|
+
else if (args[i] === '--json') result.json = true;
|
|
18
|
+
else if (args[i] === '-h' || args[i] === '--help') { result.help = true; }
|
|
19
|
+
else if (args[i] === '--mode' && args[i + 1]) { result.mode = args[++i]; }
|
|
20
|
+
else if (args[i] === '--exclude') {
|
|
21
|
+
result.exclude = result.exclude || [];
|
|
22
|
+
while (i + 1 < args.length && !args[i + 1].startsWith('-')) result.exclude.push(args[++i]);
|
|
23
|
+
}
|
|
24
|
+
else if (!args[i].startsWith('-')) result.target = args[i];
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// --- 报告格式化 ---
|
|
30
|
+
|
|
31
|
+
const SEP = '='.repeat(60);
|
|
32
|
+
const DASH = '-'.repeat(40);
|
|
33
|
+
const ICONS = {
|
|
34
|
+
error: '\u2717', warning: '\u26A0', info: '\u2139',
|
|
35
|
+
critical: '\u{1F534}', high: '\u{1F7E0}', medium: '\u{1F7E1}', low: '\u{1F535}'
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function reportHeader(title, fields) {
|
|
39
|
+
const lines = [SEP, title, SEP];
|
|
40
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
41
|
+
lines.push(`\n${k}: ${v}`);
|
|
42
|
+
}
|
|
43
|
+
return lines;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function reportIssues(issues, verbose, groupBy) {
|
|
47
|
+
if (!issues.length) return [];
|
|
48
|
+
const lines = ['\n' + DASH, '问题列表:', DASH];
|
|
49
|
+
|
|
50
|
+
if (groupBy) {
|
|
51
|
+
const groups = {};
|
|
52
|
+
for (const i of issues) (groups[i[groupBy]] || (groups[i[groupBy]] = [])).push(i);
|
|
53
|
+
for (const cat of Object.keys(groups).sort()) {
|
|
54
|
+
const items = groups[cat];
|
|
55
|
+
lines.push(`\n【${cat}】(${items.length} 个)`);
|
|
56
|
+
for (const i of items.slice(0, 10)) {
|
|
57
|
+
lines.push(` ${ICONS[i.severity] || '\u2139'} ` +
|
|
58
|
+
`${i.file_path || ''}${i.line_number ? ':' + i.line_number : ''}`);
|
|
59
|
+
lines.push(` ${i.message}`);
|
|
60
|
+
if (verbose && i.suggestion) lines.push(` \u{1F4A1} ${i.suggestion}`);
|
|
61
|
+
if (verbose && i.recommendation) lines.push(` \u{1F4A1} ${i.recommendation}`);
|
|
62
|
+
}
|
|
63
|
+
if (items.length > 10) lines.push(` ... 及其他 ${items.length - 10} 个问题`);
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
for (const i of issues) {
|
|
67
|
+
const icon = ICONS[i.severity] || '\u2139';
|
|
68
|
+
lines.push(` ${icon} [${i.severity.toUpperCase()}] ${i.message}`);
|
|
69
|
+
if (i.path && verbose) lines.push(` 路径: ${i.path}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return lines;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function reportFooter() { return ['\n' + SEP]; }
|
|
76
|
+
|
|
77
|
+
function buildReport(title, fields, issues, verbose, groupBy) {
|
|
78
|
+
return [...reportHeader(title, fields), ...reportIssues(issues, verbose, groupBy), ...reportFooter()].join('\n');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// --- 通用计数 ---
|
|
82
|
+
|
|
83
|
+
function countBySeverity(issues, field) {
|
|
84
|
+
field = field || 'severity';
|
|
85
|
+
const counts = {};
|
|
86
|
+
for (const i of issues) counts[i[field]] = (counts[i[field]] || 0) + 1;
|
|
87
|
+
return counts;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function hasFatal(issues, fatalLevels) {
|
|
91
|
+
fatalLevels = fatalLevels || ['error'];
|
|
92
|
+
return issues.some(i => fatalLevels.includes(i.severity));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = {
|
|
96
|
+
parseCliArgs, buildReport, reportHeader, reportIssues,
|
|
97
|
+
reportFooter, countBySeverity, hasFatal, SEP, DASH, ICONS
|
|
98
|
+
};
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
const { execSync } = require("child_process");
|
|
5
5
|
const path = require("path");
|
|
6
6
|
const fs = require("fs");
|
|
7
|
+
const { parseCliArgs, buildReport, hasFatal, DASH } = require(path.join(__dirname, '..', '..', 'lib', 'shared.js'));
|
|
7
8
|
|
|
8
9
|
const CODE_EXT = new Set([".py",".go",".rs",".ts",".js",".jsx",".tsx",".java",".c",".cpp",".h",".hpp"]);
|
|
9
10
|
const DOC_EXT = new Set([".md",".rst",".txt",".adoc"]);
|
|
@@ -61,7 +62,7 @@ function parsePorcelainLine(line) {
|
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
function git(args) {
|
|
64
|
-
try { return execSync(
|
|
65
|
+
try { return execSync('git ' + args, { encoding: "utf8", stdio: ["pipe","pipe","pipe"] }); }
|
|
65
66
|
catch { return ""; }
|
|
66
67
|
}
|
|
67
68
|
|
|
@@ -146,14 +147,22 @@ function checkDocSync(changes, modules) {
|
|
|
146
147
|
if (!modCode.length) continue;
|
|
147
148
|
const total = modCode.reduce((s, c) => s + c.additions + c.deletions, 0);
|
|
148
149
|
if (total > 50 && !docPaths.has(design)) {
|
|
149
|
-
issues.push({
|
|
150
|
+
issues.push({
|
|
151
|
+
severity: "warning",
|
|
152
|
+
message: `模块 ${mod} 有较大代码变更 (${total} 行),但 DESIGN.md 未更新`,
|
|
153
|
+
related_files: modCode.map(c => c.path)
|
|
154
|
+
});
|
|
150
155
|
docStatus[`${mod}/DESIGN.md`] = false;
|
|
151
156
|
} else {
|
|
152
157
|
docStatus[`${mod}/DESIGN.md`] = true;
|
|
153
158
|
}
|
|
154
159
|
const newFiles = modCode.filter(c => c.type === "added");
|
|
155
160
|
if (newFiles.length && !docPaths.has(readme)) {
|
|
156
|
-
issues.push({
|
|
161
|
+
issues.push({
|
|
162
|
+
severity: "info",
|
|
163
|
+
message: `模块 ${mod} 新增了文件,建议更新 README.md`,
|
|
164
|
+
related_files: newFiles.map(c => c.path)
|
|
165
|
+
});
|
|
157
166
|
}
|
|
158
167
|
}
|
|
159
168
|
return { docStatus, issues };
|
|
@@ -165,12 +174,30 @@ function analyzeImpact(changes) {
|
|
|
165
174
|
const tests = changes.filter(c => c.is_test);
|
|
166
175
|
if (code.length && !tests.length) {
|
|
167
176
|
const total = code.reduce((s, c) => s + c.additions + c.deletions, 0);
|
|
168
|
-
if (total > 30)
|
|
177
|
+
if (total > 30) {
|
|
178
|
+
issues.push({
|
|
179
|
+
severity: "warning",
|
|
180
|
+
message: `代码变更 ${total} 行,但没有对应的测试更新`,
|
|
181
|
+
related_files: code.map(c => c.path)
|
|
182
|
+
});
|
|
183
|
+
}
|
|
169
184
|
}
|
|
170
185
|
const configs = changes.filter(c => c.is_config);
|
|
171
|
-
if (configs.length)
|
|
186
|
+
if (configs.length) {
|
|
187
|
+
issues.push({
|
|
188
|
+
severity: "info",
|
|
189
|
+
message: "配置文件有变更,请确认是否需要更新文档",
|
|
190
|
+
related_files: configs.map(c => c.path)
|
|
191
|
+
});
|
|
192
|
+
}
|
|
172
193
|
const deleted = changes.filter(c => c.type === "deleted");
|
|
173
|
-
if (deleted.length)
|
|
194
|
+
if (deleted.length) {
|
|
195
|
+
issues.push({
|
|
196
|
+
severity: "info",
|
|
197
|
+
message: `删除了 ${deleted.length} 个文件,请确认相关引用已清理`,
|
|
198
|
+
related_files: deleted.map(c => c.path)
|
|
199
|
+
});
|
|
200
|
+
}
|
|
174
201
|
return issues;
|
|
175
202
|
}
|
|
176
203
|
|
|
@@ -195,18 +222,17 @@ function analyzeChanges(mode = "working") {
|
|
|
195
222
|
}
|
|
196
223
|
|
|
197
224
|
function formatReport(r, verbose) {
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
L.push(`分析结果: ${r.passed ? "✓ 通过" : "✗ 需要关注"}`);
|
|
225
|
+
const fields = {
|
|
226
|
+
'变更文件': r.changes.length,
|
|
227
|
+
'新增行数': `+${r.totalAdd}`,
|
|
228
|
+
'删除行数': `-${r.totalDel}`,
|
|
229
|
+
'受影响模块': [...r.modules].join(", ") || "无",
|
|
230
|
+
'分析结果': r.passed ? "✓ 通过" : "✗ 需要关注",
|
|
231
|
+
};
|
|
232
|
+
let report = buildReport('变更分析报告', fields, r.issues, verbose);
|
|
207
233
|
|
|
208
234
|
if (r.changes.length && verbose) {
|
|
209
|
-
|
|
235
|
+
const lines = ["\n" + DASH, "变更文件列表:", DASH];
|
|
210
236
|
const icons = { added: "➕", modified: "📝", deleted: "➖", renamed: "📋" };
|
|
211
237
|
for (const c of r.changes) {
|
|
212
238
|
const tags = [];
|
|
@@ -215,56 +241,49 @@ function formatReport(r, verbose) {
|
|
|
215
241
|
if (c.is_test) tags.push("测试");
|
|
216
242
|
if (c.is_config) tags.push("配置");
|
|
217
243
|
const t = tags.length ? ` [${tags.join(", ")}]` : "";
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
if (r.issues.length) {
|
|
223
|
-
L.push("\n" + sep2, "问题与建议:", sep2);
|
|
224
|
-
const si = { error: "✗", warning: "⚠", info: "ℹ" };
|
|
225
|
-
for (const i of r.issues) {
|
|
226
|
-
L.push(`\n ${si[i.severity] || "ℹ"} [${i.severity.toUpperCase()}] ${i.message}`);
|
|
227
|
-
if (verbose && i.related_files.length) {
|
|
228
|
-
for (const f of i.related_files.slice(0, 5)) L.push(` - ${f}`);
|
|
229
|
-
if (i.related_files.length > 5) L.push(` ... 及其他 ${i.related_files.length - 5} 个文件`);
|
|
230
|
-
}
|
|
244
|
+
lines.push(` ${icons[c.type] || "📝"} ${c.path}${t} (+${c.additions}/-${c.deletions})`);
|
|
231
245
|
}
|
|
246
|
+
report += '\n' + lines.join('\n');
|
|
232
247
|
}
|
|
233
248
|
|
|
234
249
|
if (Object.keys(r.docStatus).length) {
|
|
235
|
-
|
|
250
|
+
const lines = ["\n" + DASH, "文档同步状态:", DASH];
|
|
236
251
|
for (const [doc, synced] of Object.entries(r.docStatus)) {
|
|
237
|
-
|
|
252
|
+
lines.push(` ${synced ? "✓" : "✗"} ${doc}`);
|
|
238
253
|
}
|
|
254
|
+
report += '\n' + lines.join('\n');
|
|
239
255
|
}
|
|
240
256
|
|
|
241
|
-
|
|
242
|
-
return L.join("\n");
|
|
257
|
+
return report;
|
|
243
258
|
}
|
|
244
259
|
|
|
245
260
|
// CLI
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if (args[i] === "--mode" && args[i + 1]) { mode = args[++i]; }
|
|
250
|
-
else if (args[i] === "-v" || args[i] === "--verbose") { verbose = true; }
|
|
251
|
-
else if (args[i] === "--json") { jsonOut = true; }
|
|
252
|
-
}
|
|
261
|
+
if (require.main === module) {
|
|
262
|
+
const opts = parseCliArgs(process.argv);
|
|
263
|
+
const result = analyzeChanges(opts.mode || "working");
|
|
253
264
|
|
|
254
|
-
|
|
265
|
+
if (opts.json) {
|
|
266
|
+
console.log(JSON.stringify({
|
|
267
|
+
passed: result.passed,
|
|
268
|
+
total_additions: result.totalAdd,
|
|
269
|
+
total_deletions: result.totalDel,
|
|
270
|
+
affected_modules: [...result.modules],
|
|
271
|
+
changes: result.changes.map(c => ({
|
|
272
|
+
path: c.path, type: c.type, additions: c.additions,
|
|
273
|
+
deletions: c.deletions, is_code: c.is_code,
|
|
274
|
+
is_doc: c.is_doc, is_test: c.is_test
|
|
275
|
+
})),
|
|
276
|
+
issues: result.issues.map(i => ({
|
|
277
|
+
severity: i.severity, message: i.message,
|
|
278
|
+
related_files: i.related_files,
|
|
279
|
+
})),
|
|
280
|
+
doc_sync_status: result.docStatus,
|
|
281
|
+
}, null, 2));
|
|
282
|
+
} else {
|
|
283
|
+
console.log(formatReport(result, opts.verbose));
|
|
284
|
+
}
|
|
255
285
|
|
|
256
|
-
|
|
257
|
-
console.log(JSON.stringify({
|
|
258
|
-
passed: result.passed,
|
|
259
|
-
total_additions: result.totalAdd,
|
|
260
|
-
total_deletions: result.totalDel,
|
|
261
|
-
affected_modules: [...result.modules],
|
|
262
|
-
changes: result.changes.map(c => ({ path: c.path, type: c.type, additions: c.additions, deletions: c.deletions, is_code: c.is_code, is_doc: c.is_doc, is_test: c.is_test })),
|
|
263
|
-
issues: result.issues.map(i => ({ severity: i.severity, message: i.message, related_files: i.related_files })),
|
|
264
|
-
doc_sync_status: result.docStatus,
|
|
265
|
-
}, null, 2));
|
|
266
|
-
} else {
|
|
267
|
-
console.log(formatReport(result, verbose));
|
|
286
|
+
process.exit(result.passed ? 0 : 1);
|
|
268
287
|
}
|
|
269
288
|
|
|
270
|
-
|
|
289
|
+
module.exports = { normalizePath, classifyFile, parsePorcelainLine, parseNameStatusLine, identifyModules };
|
|
@@ -3,11 +3,15 @@
|
|
|
3
3
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
|
+
const { parseCliArgs, buildReport, hasFatal } = require(path.join(__dirname, '..', '..', 'lib', 'shared.js'));
|
|
6
7
|
|
|
7
8
|
const REQUIRED_FILES = { 'README.md': '模块说明文档', 'DESIGN.md': '设计决策文档' };
|
|
8
9
|
const ALT_SRC_DIRS = ['src', 'lib', 'pkg', 'internal', 'cmd', 'app'];
|
|
9
10
|
const ALT_TEST_DIRS = ['tests', 'test', '__tests__', 'spec'];
|
|
10
|
-
const ROOT_SCRIPT_FILES = new Set([
|
|
11
|
+
const ROOT_SCRIPT_FILES = new Set([
|
|
12
|
+
'install.sh', 'uninstall.sh', 'install.ps1',
|
|
13
|
+
'uninstall.ps1', 'Dockerfile', 'Makefile'
|
|
14
|
+
]);
|
|
11
15
|
const CODE_EXTS = new Set(['.py', '.go', '.rs', '.ts', '.js', '.java', '.sh', '.ps1']);
|
|
12
16
|
const TEST_PATTERNS = ['test_', '_test.', '.test.', 'spec_', '_spec.'];
|
|
13
17
|
|
|
@@ -45,8 +49,14 @@ function scanModule(target) {
|
|
|
45
49
|
const issues = [];
|
|
46
50
|
const add = (severity, message, p) => issues.push({ severity, message, path: p || null });
|
|
47
51
|
|
|
48
|
-
if (!fs.existsSync(modulePath)) {
|
|
49
|
-
|
|
52
|
+
if (!fs.existsSync(modulePath)) {
|
|
53
|
+
add('error', `路径不存在: ${modulePath}`);
|
|
54
|
+
return { modulePath, issues, structure: {} };
|
|
55
|
+
}
|
|
56
|
+
if (!fs.statSync(modulePath).isDirectory()) {
|
|
57
|
+
add('error', `不是目录: ${modulePath}`);
|
|
58
|
+
return { modulePath, issues, structure: {} };
|
|
59
|
+
}
|
|
50
60
|
|
|
51
61
|
const structure = scanStructure(modulePath);
|
|
52
62
|
|
|
@@ -58,15 +68,36 @@ function scanModule(target) {
|
|
|
58
68
|
}
|
|
59
69
|
|
|
60
70
|
// source dirs
|
|
61
|
-
let srcFound = ALT_SRC_DIRS.some(d => {
|
|
71
|
+
let srcFound = ALT_SRC_DIRS.some(d => {
|
|
72
|
+
try { return fs.statSync(path.join(modulePath, d)).isDirectory(); }
|
|
73
|
+
catch { return false; }
|
|
74
|
+
});
|
|
62
75
|
const entries = fs.readdirSync(modulePath);
|
|
63
|
-
const rootCode = entries.filter(n => {
|
|
64
|
-
|
|
65
|
-
|
|
76
|
+
const rootCode = entries.filter(n => {
|
|
77
|
+
try {
|
|
78
|
+
const s = fs.statSync(path.join(modulePath, n));
|
|
79
|
+
return s.isFile() && CODE_EXTS.has(path.extname(n));
|
|
80
|
+
} catch { return false; }
|
|
81
|
+
});
|
|
82
|
+
const rootScript = entries.filter(n => {
|
|
83
|
+
try {
|
|
84
|
+
return fs.statSync(path.join(modulePath, n)).isFile()
|
|
85
|
+
&& ROOT_SCRIPT_FILES.has(n);
|
|
86
|
+
} catch { return false; }
|
|
87
|
+
});
|
|
88
|
+
if (rootCode.length || rootScript.length) {
|
|
89
|
+
srcFound = true;
|
|
90
|
+
if (rootCode.length > 5) {
|
|
91
|
+
add('warning', `根目录代码文件过多 (${rootCode.length}个),建议整理到 src/ 目录`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
66
94
|
if (!srcFound) add('warning', '未找到源码目录或代码文件');
|
|
67
95
|
|
|
68
96
|
// test dirs
|
|
69
|
-
let testFound = ALT_TEST_DIRS.some(d => {
|
|
97
|
+
let testFound = ALT_TEST_DIRS.some(d => {
|
|
98
|
+
try { return fs.statSync(path.join(modulePath, d)).isDirectory(); }
|
|
99
|
+
catch { return false; }
|
|
100
|
+
});
|
|
70
101
|
if (!testFound) testFound = rglob(modulePath, n => TEST_PATTERNS.some(p => n.includes(p)));
|
|
71
102
|
if (!testFound) add('warning', '未找到测试目录或测试文件');
|
|
72
103
|
|
|
@@ -75,12 +106,16 @@ function scanModule(target) {
|
|
|
75
106
|
if (fs.existsSync(readme)) {
|
|
76
107
|
const c = fs.readFileSync(readme, 'utf-8');
|
|
77
108
|
if (!c.includes('#')) add('warning', 'README.md 缺少标题', readme);
|
|
78
|
-
|
|
109
|
+
const docKeys = ['usage', 'install', '使用', '安装', 'example', '示例'];
|
|
110
|
+
if (!docKeys.some(k => c.toLowerCase().includes(k)))
|
|
111
|
+
add('info', 'README.md 建议添加使用说明或示例', readme);
|
|
79
112
|
}
|
|
80
113
|
const design = path.join(modulePath, 'DESIGN.md');
|
|
81
114
|
if (fs.existsSync(design)) {
|
|
82
115
|
const c = fs.readFileSync(design, 'utf-8');
|
|
83
|
-
|
|
116
|
+
const designKeys = ['决策', 'decision', '选择', 'choice', '权衡', 'trade'];
|
|
117
|
+
if (!designKeys.some(k => c.toLowerCase().includes(k)))
|
|
118
|
+
add('info', 'DESIGN.md 建议记录设计决策和权衡', design);
|
|
84
119
|
}
|
|
85
120
|
|
|
86
121
|
return { modulePath, issues, structure };
|
|
@@ -98,40 +133,31 @@ function formatStructure(s, indent = 0) {
|
|
|
98
133
|
}
|
|
99
134
|
|
|
100
135
|
function formatReport(r, verbose) {
|
|
101
|
-
const passed = !r.issues.some(i => i.severity === 'error');
|
|
102
136
|
const errs = r.issues.filter(i => i.severity === 'error').length;
|
|
103
137
|
const warns = r.issues.filter(i => i.severity === 'warning').length;
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
`错误: ${errs} | 警告: ${warns}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (i.path && verbose) lines.push(` 路径: ${i.path}`);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
138
|
+
const passed = !hasFatal(r.issues);
|
|
139
|
+
const fields = {
|
|
140
|
+
'模块路径': r.modulePath,
|
|
141
|
+
'扫描结果': passed ? '\u2713 通过' : '\u2717 未通过',
|
|
142
|
+
'统计': `错误: ${errs} | 警告: ${warns}`,
|
|
143
|
+
};
|
|
144
|
+
const issues = r.issues.map(i => ({
|
|
145
|
+
severity: i.severity, message: i.message, path: i.path,
|
|
146
|
+
file_path: i.path || '', line_number: null,
|
|
147
|
+
}));
|
|
148
|
+
let report = buildReport('模块完整性扫描报告', fields, issues, verbose);
|
|
118
149
|
if (verbose && r.structure.name) {
|
|
119
|
-
|
|
150
|
+
report += '\n' + '-'.repeat(40) + '\n目录结构:\n' + '-'.repeat(40) + '\n' + formatStructure(r.structure);
|
|
120
151
|
}
|
|
121
|
-
|
|
122
|
-
return lines.join('\n');
|
|
152
|
+
return report;
|
|
123
153
|
}
|
|
124
154
|
|
|
125
155
|
// CLI
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
const target = args.find(a => !a.startsWith('-')) || '.';
|
|
130
|
-
|
|
131
|
-
const result = scanModule(target);
|
|
132
|
-
const passed = !result.issues.some(i => i.severity === 'error');
|
|
156
|
+
const opts = parseCliArgs(process.argv);
|
|
157
|
+
const result = scanModule(opts.target);
|
|
158
|
+
const passed = !hasFatal(result.issues);
|
|
133
159
|
|
|
134
|
-
if (
|
|
160
|
+
if (opts.json) {
|
|
135
161
|
console.log(JSON.stringify({
|
|
136
162
|
module_path: result.modulePath, passed,
|
|
137
163
|
error_count: result.issues.filter(i => i.severity === 'error').length,
|
|
@@ -139,7 +165,7 @@ if (jsonOut) {
|
|
|
139
165
|
issues: result.issues
|
|
140
166
|
}, null, 2));
|
|
141
167
|
} else {
|
|
142
|
-
console.log(formatReport(result, verbose));
|
|
168
|
+
console.log(formatReport(result, opts.verbose));
|
|
143
169
|
}
|
|
144
170
|
|
|
145
171
|
process.exit(passed ? 0 : 1);
|