code-abyss 1.6.16 → 1.7.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.
- 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/SKILL.md +24 -16
- package/skills/domains/ai/SKILL.md +2 -2
- package/skills/domains/ai/prompt-and-eval.md +279 -0
- package/skills/domains/architecture/SKILL.md +2 -3
- package/skills/domains/architecture/security-arch.md +87 -0
- package/skills/domains/data-engineering/SKILL.md +188 -26
- package/skills/domains/development/SKILL.md +1 -4
- package/skills/domains/devops/SKILL.md +3 -5
- package/skills/domains/devops/performance.md +63 -0
- package/skills/domains/devops/testing.md +97 -0
- package/skills/domains/frontend-design/SKILL.md +12 -3
- package/skills/domains/frontend-design/claymorphism/SKILL.md +117 -0
- package/skills/domains/frontend-design/claymorphism/references/tokens.css +52 -0
- package/skills/domains/frontend-design/engineering.md +287 -0
- package/skills/domains/frontend-design/glassmorphism/SKILL.md +138 -0
- package/skills/domains/frontend-design/glassmorphism/references/tokens.css +32 -0
- package/skills/domains/frontend-design/liquid-glass/SKILL.md +135 -0
- package/skills/domains/frontend-design/liquid-glass/references/tokens.css +81 -0
- package/skills/domains/frontend-design/neubrutalism/SKILL.md +141 -0
- package/skills/domains/frontend-design/neubrutalism/references/tokens.css +44 -0
- package/skills/domains/infrastructure/SKILL.md +174 -34
- package/skills/domains/mobile/SKILL.md +211 -21
- package/skills/domains/orchestration/SKILL.md +1 -0
- package/skills/domains/security/SKILL.md +4 -6
- package/skills/domains/security/blue-team.md +57 -0
- package/skills/domains/security/red-team.md +54 -0
- package/skills/domains/security/threat-intel.md +50 -0
- package/skills/orchestration/multi-agent/SKILL.md +195 -46
- package/skills/run_skill.js +139 -0
- package/skills/tools/gen-docs/SKILL.md +6 -4
- package/skills/tools/gen-docs/scripts/doc_generator.js +363 -0
- package/skills/tools/lib/shared.js +98 -0
- package/skills/tools/verify-change/SKILL.md +8 -6
- package/skills/tools/verify-change/scripts/change_analyzer.js +289 -0
- package/skills/tools/verify-module/SKILL.md +6 -4
- package/skills/tools/verify-module/scripts/module_scanner.js +171 -0
- package/skills/tools/verify-quality/SKILL.md +5 -3
- package/skills/tools/verify-quality/scripts/quality_checker.js +337 -0
- package/skills/tools/verify-security/SKILL.md +7 -5
- package/skills/tools/verify-security/scripts/security_scanner.js +283 -0
- package/skills/__pycache__/run_skill.cpython-312.pyc +0 -0
- package/skills/domains/COVERAGE_PLAN.md +0 -232
- package/skills/domains/ai/model-evaluation.md +0 -790
- package/skills/domains/ai/prompt-engineering.md +0 -703
- package/skills/domains/architecture/compliance.md +0 -299
- package/skills/domains/architecture/data-security.md +0 -184
- package/skills/domains/data-engineering/data-pipeline.md +0 -762
- package/skills/domains/data-engineering/data-quality.md +0 -894
- package/skills/domains/data-engineering/stream-processing.md +0 -791
- package/skills/domains/development/dart.md +0 -963
- package/skills/domains/development/kotlin.md +0 -834
- package/skills/domains/development/php.md +0 -659
- package/skills/domains/development/swift.md +0 -755
- package/skills/domains/devops/e2e-testing.md +0 -914
- package/skills/domains/devops/performance-testing.md +0 -734
- package/skills/domains/devops/testing-strategy.md +0 -667
- package/skills/domains/frontend-design/build-tools.md +0 -743
- package/skills/domains/frontend-design/performance.md +0 -734
- package/skills/domains/frontend-design/testing.md +0 -699
- package/skills/domains/infrastructure/gitops.md +0 -735
- package/skills/domains/infrastructure/iac.md +0 -855
- package/skills/domains/infrastructure/kubernetes.md +0 -1018
- package/skills/domains/mobile/android-dev.md +0 -979
- package/skills/domains/mobile/cross-platform.md +0 -795
- package/skills/domains/mobile/ios-dev.md +0 -931
- package/skills/domains/security/secrets-management.md +0 -834
- package/skills/domains/security/supply-chain.md +0 -931
- package/skills/domains/security/threat-modeling.md +0 -828
- package/skills/run_skill.py +0 -153
- package/skills/tests/README.md +0 -225
- package/skills/tests/SUMMARY.md +0 -362
- package/skills/tests/__init__.py +0 -3
- package/skills/tests/__pycache__/test_change_analyzer.cpython-312.pyc +0 -0
- package/skills/tests/__pycache__/test_doc_generator.cpython-312.pyc +0 -0
- package/skills/tests/__pycache__/test_module_scanner.cpython-312.pyc +0 -0
- package/skills/tests/__pycache__/test_quality_checker.cpython-312.pyc +0 -0
- package/skills/tests/__pycache__/test_security_scanner.cpython-312.pyc +0 -0
- package/skills/tests/test_change_analyzer.py +0 -558
- package/skills/tests/test_doc_generator.py +0 -538
- package/skills/tests/test_module_scanner.py +0 -376
- package/skills/tests/test_quality_checker.py +0 -516
- package/skills/tests/test_security_scanner.py +0 -426
- package/skills/tools/gen-docs/scripts/__pycache__/doc_generator.cpython-312.pyc +0 -0
- package/skills/tools/gen-docs/scripts/doc_generator.py +0 -520
- package/skills/tools/verify-change/scripts/__pycache__/change_analyzer.cpython-312.pyc +0 -0
- package/skills/tools/verify-change/scripts/change_analyzer.py +0 -529
- package/skills/tools/verify-module/scripts/__pycache__/module_scanner.cpython-312.pyc +0 -0
- package/skills/tools/verify-module/scripts/module_scanner.py +0 -321
- package/skills/tools/verify-quality/scripts/__pycache__/quality_checker.cpython-312.pyc +0 -0
- package/skills/tools/verify-quality/scripts/quality_checker.py +0 -481
- package/skills/tools/verify-security/scripts/__pycache__/security_scanner.cpython-312.pyc +0 -0
- package/skills/tools/verify-security/scripts/security_scanner.py +0 -374
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const { execSync } = require("child_process");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const { parseCliArgs, buildReport, hasFatal, DASH } = require(path.join(__dirname, '..', '..', 'lib', 'shared.js'));
|
|
8
|
+
|
|
9
|
+
const CODE_EXT = new Set([".py",".go",".rs",".ts",".js",".jsx",".tsx",".java",".c",".cpp",".h",".hpp"]);
|
|
10
|
+
const DOC_EXT = new Set([".md",".rst",".txt",".adoc"]);
|
|
11
|
+
const TEST_PATTERNS = ["test_","_test.",".test.","spec_","_spec.","/tests/","/test/","/__tests__/"];
|
|
12
|
+
const CONFIG_FILES = new Set(["package.json","pyproject.toml","go.mod","cargo.toml","pom.xml","makefile","dockerfile"]);
|
|
13
|
+
const CONFIG_EXT = new Set([".yaml",".yml",".json",".toml",".ini"]);
|
|
14
|
+
|
|
15
|
+
function normalizePath(p) {
|
|
16
|
+
let s = p.trim();
|
|
17
|
+
if (s.startsWith('"') && s.endsWith('"') && s.length >= 2) {
|
|
18
|
+
s = s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
19
|
+
}
|
|
20
|
+
if (s.startsWith("./")) s = s.slice(2);
|
|
21
|
+
return s;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function classifyFile(filePath) {
|
|
25
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
26
|
+
const name = path.basename(filePath).toLowerCase();
|
|
27
|
+
const lower = filePath.toLowerCase();
|
|
28
|
+
return {
|
|
29
|
+
path: filePath, type: "modified", additions: 0, deletions: 0,
|
|
30
|
+
is_code: CODE_EXT.has(ext), is_doc: DOC_EXT.has(ext),
|
|
31
|
+
is_test: TEST_PATTERNS.some(p => lower.includes(p)),
|
|
32
|
+
is_config: CONFIG_FILES.has(name) || CONFIG_EXT.has(ext),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseNameStatusLine(line) {
|
|
37
|
+
const parts = line.split("\t");
|
|
38
|
+
if (parts.length < 2) return null;
|
|
39
|
+
const status = parts[0][0];
|
|
40
|
+
const p = normalizePath(parts[parts.length - 1]);
|
|
41
|
+
if (!p) return null;
|
|
42
|
+
const c = classifyFile(p);
|
|
43
|
+
const map = { A: "added", M: "modified", D: "deleted", R: "renamed" };
|
|
44
|
+
if (map[status]) c.type = map[status];
|
|
45
|
+
return c;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parsePorcelainLine(line) {
|
|
49
|
+
if (line.length < 3) return null;
|
|
50
|
+
const status = line.slice(0, 2);
|
|
51
|
+
let raw = line.slice(3);
|
|
52
|
+
if (!raw) return null;
|
|
53
|
+
if (raw.includes(" -> ")) raw = raw.split(" -> ")[1];
|
|
54
|
+
const p = normalizePath(raw);
|
|
55
|
+
if (!p) return null;
|
|
56
|
+
const c = classifyFile(p);
|
|
57
|
+
if (status.includes("?") || status.includes("A")) c.type = "added";
|
|
58
|
+
else if (status.includes("R")) c.type = "renamed";
|
|
59
|
+
else if (status.includes("M")) c.type = "modified";
|
|
60
|
+
else if (status.includes("D")) c.type = "deleted";
|
|
61
|
+
return c;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function git(args) {
|
|
65
|
+
try { return execSync('git ' + args, { encoding: "utf8", stdio: ["pipe","pipe","pipe"] }); }
|
|
66
|
+
catch { return ""; }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getGitChanges(base = "HEAD~1", target = "HEAD") {
|
|
70
|
+
const changes = [];
|
|
71
|
+
for (const line of git(`diff --name-status ${base} ${target}`).split("\n")) {
|
|
72
|
+
if (!line) continue;
|
|
73
|
+
const c = parseNameStatusLine(line);
|
|
74
|
+
if (c) changes.push(c);
|
|
75
|
+
}
|
|
76
|
+
const statMap = {};
|
|
77
|
+
for (const line of git(`diff --numstat ${base} ${target}`).split("\n")) {
|
|
78
|
+
if (!line) continue;
|
|
79
|
+
const parts = line.split("\t");
|
|
80
|
+
if (parts.length >= 3) {
|
|
81
|
+
statMap[normalizePath(parts[2])] = [
|
|
82
|
+
parts[0] === "-" ? 0 : parseInt(parts[0], 10),
|
|
83
|
+
parts[1] === "-" ? 0 : parseInt(parts[1], 10),
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
for (const c of changes) {
|
|
88
|
+
if (statMap[c.path]) { [c.additions, c.deletions] = statMap[c.path]; }
|
|
89
|
+
}
|
|
90
|
+
return changes;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function getStagedChanges() {
|
|
94
|
+
const changes = [];
|
|
95
|
+
for (const line of git("diff --cached --name-status").split("\n")) {
|
|
96
|
+
if (!line) continue;
|
|
97
|
+
const c = parseNameStatusLine(line);
|
|
98
|
+
if (c) changes.push(c);
|
|
99
|
+
}
|
|
100
|
+
return changes;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function getWorkingChanges() {
|
|
104
|
+
const changes = [];
|
|
105
|
+
for (const line of git("status --porcelain").split("\n")) {
|
|
106
|
+
if (!line) continue;
|
|
107
|
+
const c = parsePorcelainLine(line);
|
|
108
|
+
if (c) changes.push(c);
|
|
109
|
+
}
|
|
110
|
+
return changes;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isPathInModule(filePath, mod) {
|
|
114
|
+
const np = normalizePath(filePath);
|
|
115
|
+
if (mod === ".") return !np.includes("/");
|
|
116
|
+
return np === mod || np.startsWith(mod + "/");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function identifyModules(changes) {
|
|
120
|
+
const modules = new Set();
|
|
121
|
+
for (const c of changes) {
|
|
122
|
+
const np = normalizePath(c.path);
|
|
123
|
+
const parts = np.split("/").filter(Boolean);
|
|
124
|
+
if (parts.length === 1) { modules.add("."); continue; }
|
|
125
|
+
let found = false;
|
|
126
|
+
for (let i = 0; i < parts.length; i++) {
|
|
127
|
+
const mp = parts.slice(0, i + 1).join("/");
|
|
128
|
+
if (fs.existsSync(path.join(mp, "README.md")) || fs.existsSync(path.join(mp, "DESIGN.md"))) {
|
|
129
|
+
modules.add(mp); found = true; break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (!found && parts.length > 1) modules.add(parts[0]);
|
|
133
|
+
}
|
|
134
|
+
return modules;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function checkDocSync(changes, modules) {
|
|
138
|
+
const docStatus = {};
|
|
139
|
+
const issues = [];
|
|
140
|
+
const codeChanges = changes.filter(c => c.is_code && c.type !== "deleted");
|
|
141
|
+
const docPaths = new Set(changes.filter(c => c.is_doc).map(c => normalizePath(c.path)));
|
|
142
|
+
|
|
143
|
+
for (const mod of modules) {
|
|
144
|
+
const readme = normalizePath(mod === "." ? "README.md" : `${mod}/README.md`);
|
|
145
|
+
const design = normalizePath(mod === "." ? "DESIGN.md" : `${mod}/DESIGN.md`);
|
|
146
|
+
const modCode = codeChanges.filter(c => isPathInModule(c.path, mod));
|
|
147
|
+
if (!modCode.length) continue;
|
|
148
|
+
const total = modCode.reduce((s, c) => s + c.additions + c.deletions, 0);
|
|
149
|
+
if (total > 50 && !docPaths.has(design)) {
|
|
150
|
+
issues.push({
|
|
151
|
+
severity: "warning",
|
|
152
|
+
message: `模块 ${mod} 有较大代码变更 (${total} 行),但 DESIGN.md 未更新`,
|
|
153
|
+
related_files: modCode.map(c => c.path)
|
|
154
|
+
});
|
|
155
|
+
docStatus[`${mod}/DESIGN.md`] = false;
|
|
156
|
+
} else {
|
|
157
|
+
docStatus[`${mod}/DESIGN.md`] = true;
|
|
158
|
+
}
|
|
159
|
+
const newFiles = modCode.filter(c => c.type === "added");
|
|
160
|
+
if (newFiles.length && !docPaths.has(readme)) {
|
|
161
|
+
issues.push({
|
|
162
|
+
severity: "info",
|
|
163
|
+
message: `模块 ${mod} 新增了文件,建议更新 README.md`,
|
|
164
|
+
related_files: newFiles.map(c => c.path)
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return { docStatus, issues };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function analyzeImpact(changes) {
|
|
172
|
+
const issues = [];
|
|
173
|
+
const code = changes.filter(c => c.is_code && !c.is_test);
|
|
174
|
+
const tests = changes.filter(c => c.is_test);
|
|
175
|
+
if (code.length && !tests.length) {
|
|
176
|
+
const total = code.reduce((s, c) => s + c.additions + c.deletions, 0);
|
|
177
|
+
if (total > 30) {
|
|
178
|
+
issues.push({
|
|
179
|
+
severity: "warning",
|
|
180
|
+
message: `代码变更 ${total} 行,但没有对应的测试更新`,
|
|
181
|
+
related_files: code.map(c => c.path)
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const configs = changes.filter(c => c.is_config);
|
|
186
|
+
if (configs.length) {
|
|
187
|
+
issues.push({
|
|
188
|
+
severity: "info",
|
|
189
|
+
message: "配置文件有变更,请确认是否需要更新文档",
|
|
190
|
+
related_files: configs.map(c => c.path)
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
const deleted = changes.filter(c => c.type === "deleted");
|
|
194
|
+
if (deleted.length) {
|
|
195
|
+
issues.push({
|
|
196
|
+
severity: "info",
|
|
197
|
+
message: `删除了 ${deleted.length} 个文件,请确认相关引用已清理`,
|
|
198
|
+
related_files: deleted.map(c => c.path)
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return issues;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function analyzeChanges(mode = "working") {
|
|
205
|
+
let changes;
|
|
206
|
+
if (mode === "staged") changes = getStagedChanges();
|
|
207
|
+
else if (mode === "committed") changes = getGitChanges();
|
|
208
|
+
else changes = getWorkingChanges();
|
|
209
|
+
|
|
210
|
+
const issues = [];
|
|
211
|
+
let modules = new Set(), docStatus = {};
|
|
212
|
+
if (changes.length) {
|
|
213
|
+
modules = identifyModules(changes);
|
|
214
|
+
const ds = checkDocSync(changes, modules);
|
|
215
|
+
docStatus = ds.docStatus;
|
|
216
|
+
issues.push(...ds.issues, ...analyzeImpact(changes));
|
|
217
|
+
}
|
|
218
|
+
const passed = !issues.some(i => i.severity === "error");
|
|
219
|
+
const totalAdd = changes.reduce((s, c) => s + c.additions, 0);
|
|
220
|
+
const totalDel = changes.reduce((s, c) => s + c.deletions, 0);
|
|
221
|
+
return { changes, issues, modules, docStatus, passed, totalAdd, totalDel };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function formatReport(r, verbose) {
|
|
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);
|
|
233
|
+
|
|
234
|
+
if (r.changes.length && verbose) {
|
|
235
|
+
const lines = ["\n" + DASH, "变更文件列表:", DASH];
|
|
236
|
+
const icons = { added: "➕", modified: "📝", deleted: "➖", renamed: "📋" };
|
|
237
|
+
for (const c of r.changes) {
|
|
238
|
+
const tags = [];
|
|
239
|
+
if (c.is_code) tags.push("代码");
|
|
240
|
+
if (c.is_doc) tags.push("文档");
|
|
241
|
+
if (c.is_test) tags.push("测试");
|
|
242
|
+
if (c.is_config) tags.push("配置");
|
|
243
|
+
const t = tags.length ? ` [${tags.join(", ")}]` : "";
|
|
244
|
+
lines.push(` ${icons[c.type] || "📝"} ${c.path}${t} (+${c.additions}/-${c.deletions})`);
|
|
245
|
+
}
|
|
246
|
+
report += '\n' + lines.join('\n');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (Object.keys(r.docStatus).length) {
|
|
250
|
+
const lines = ["\n" + DASH, "文档同步状态:", DASH];
|
|
251
|
+
for (const [doc, synced] of Object.entries(r.docStatus)) {
|
|
252
|
+
lines.push(` ${synced ? "✓" : "✗"} ${doc}`);
|
|
253
|
+
}
|
|
254
|
+
report += '\n' + lines.join('\n');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return report;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// CLI
|
|
261
|
+
if (require.main === module) {
|
|
262
|
+
const opts = parseCliArgs(process.argv);
|
|
263
|
+
const result = analyzeChanges(opts.mode || "working");
|
|
264
|
+
|
|
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
|
+
}
|
|
285
|
+
|
|
286
|
+
process.exit(result.passed ? 0 : 1);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
module.exports = { normalizePath, classifyFile, parsePorcelainLine, parseNameStatusLine, identifyModules };
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: verify-module
|
|
3
3
|
description: 模块完整性校验关卡。扫描目录结构、检测缺失文档、验证代码与文档同步。当魔尊提到模块校验、文档检查、结构完整性、README检查、DESIGN检查时使用。在新建模块完成时自动触发。
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility: node>=18
|
|
4
6
|
user-invocable: true
|
|
5
7
|
disable-model-invocation: false
|
|
6
8
|
allowed-tools: Bash, Read, Glob
|
|
@@ -23,9 +25,9 @@ argument-hint: <模块路径>
|
|
|
23
25
|
|
|
24
26
|
```bash
|
|
25
27
|
# 在 verify-module 目录下运行(推荐)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
node scripts/module_scanner.js <模块路径>
|
|
29
|
+
node scripts/module_scanner.js <模块路径> -v # 详细模式
|
|
30
|
+
node scripts/module_scanner.js <模块路径> --json # JSON 输出
|
|
29
31
|
```
|
|
30
32
|
|
|
31
33
|
## 校验标准
|
|
@@ -84,7 +86,7 @@ module/
|
|
|
84
86
|
## 校验流程
|
|
85
87
|
|
|
86
88
|
```
|
|
87
|
-
1. 运行 module_scanner.
|
|
89
|
+
1. 运行 module_scanner.js 自动扫描
|
|
88
90
|
2. 检查文件结构是否完整
|
|
89
91
|
3. 检查 README.md 各项是否齐全
|
|
90
92
|
4. 检查 DESIGN.md 各项是否齐全
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { parseCliArgs, buildReport, hasFatal } = require(path.join(__dirname, '..', '..', 'lib', 'shared.js'));
|
|
7
|
+
|
|
8
|
+
const REQUIRED_FILES = { 'README.md': '模块说明文档', 'DESIGN.md': '设计决策文档' };
|
|
9
|
+
const ALT_SRC_DIRS = ['src', 'lib', 'pkg', 'internal', 'cmd', 'app'];
|
|
10
|
+
const ALT_TEST_DIRS = ['tests', 'test', '__tests__', 'spec'];
|
|
11
|
+
const ROOT_SCRIPT_FILES = new Set([
|
|
12
|
+
'install.sh', 'uninstall.sh', 'install.ps1',
|
|
13
|
+
'uninstall.ps1', 'Dockerfile', 'Makefile'
|
|
14
|
+
]);
|
|
15
|
+
const CODE_EXTS = new Set(['.py', '.go', '.rs', '.ts', '.js', '.java', '.sh', '.ps1']);
|
|
16
|
+
const TEST_PATTERNS = ['test_', '_test.', '.test.', 'spec_', '_spec.'];
|
|
17
|
+
|
|
18
|
+
function scanStructure(p, depth = 3) {
|
|
19
|
+
const s = { name: path.basename(p), type: 'dir', children: [] };
|
|
20
|
+
if (depth <= 0) return s;
|
|
21
|
+
try {
|
|
22
|
+
for (const name of fs.readdirSync(p).sort()) {
|
|
23
|
+
if (name.startsWith('.')) continue;
|
|
24
|
+
const full = path.join(p, name);
|
|
25
|
+
const stat = fs.statSync(full);
|
|
26
|
+
if (stat.isFile()) s.children.push({ name, type: 'file', size: stat.size });
|
|
27
|
+
else if (stat.isDirectory()) s.children.push(scanStructure(full, depth - 1));
|
|
28
|
+
}
|
|
29
|
+
} catch {}
|
|
30
|
+
return s;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function rglob(dir, test) {
|
|
34
|
+
try {
|
|
35
|
+
for (const name of fs.readdirSync(dir)) {
|
|
36
|
+
const full = path.join(dir, name);
|
|
37
|
+
try {
|
|
38
|
+
const stat = fs.statSync(full);
|
|
39
|
+
if (stat.isFile() && test(name)) return true;
|
|
40
|
+
if (stat.isDirectory()) { if (rglob(full, test)) return true; }
|
|
41
|
+
} catch {}
|
|
42
|
+
}
|
|
43
|
+
} catch {}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function scanModule(target) {
|
|
48
|
+
const modulePath = path.resolve(target);
|
|
49
|
+
const issues = [];
|
|
50
|
+
const add = (severity, message, p) => issues.push({ severity, message, path: p || null });
|
|
51
|
+
|
|
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
|
+
}
|
|
60
|
+
|
|
61
|
+
const structure = scanStructure(modulePath);
|
|
62
|
+
|
|
63
|
+
// required files
|
|
64
|
+
for (const [file, desc] of Object.entries(REQUIRED_FILES)) {
|
|
65
|
+
const fp = path.join(modulePath, file);
|
|
66
|
+
if (!fs.existsSync(fp)) add('error', `缺少必需文档: ${file} (${desc})`, fp);
|
|
67
|
+
else if (fs.statSync(fp).size < 50) add('warning', `文档内容过少: ${file} (< 50 bytes)`, fp);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// source dirs
|
|
71
|
+
let srcFound = ALT_SRC_DIRS.some(d => {
|
|
72
|
+
try { return fs.statSync(path.join(modulePath, d)).isDirectory(); }
|
|
73
|
+
catch { return false; }
|
|
74
|
+
});
|
|
75
|
+
const entries = fs.readdirSync(modulePath);
|
|
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
|
+
}
|
|
94
|
+
if (!srcFound) add('warning', '未找到源码目录或代码文件');
|
|
95
|
+
|
|
96
|
+
// test dirs
|
|
97
|
+
let testFound = ALT_TEST_DIRS.some(d => {
|
|
98
|
+
try { return fs.statSync(path.join(modulePath, d)).isDirectory(); }
|
|
99
|
+
catch { return false; }
|
|
100
|
+
});
|
|
101
|
+
if (!testFound) testFound = rglob(modulePath, n => TEST_PATTERNS.some(p => n.includes(p)));
|
|
102
|
+
if (!testFound) add('warning', '未找到测试目录或测试文件');
|
|
103
|
+
|
|
104
|
+
// doc quality
|
|
105
|
+
const readme = path.join(modulePath, 'README.md');
|
|
106
|
+
if (fs.existsSync(readme)) {
|
|
107
|
+
const c = fs.readFileSync(readme, 'utf-8');
|
|
108
|
+
if (!c.includes('#')) add('warning', 'README.md 缺少标题', readme);
|
|
109
|
+
const docKeys = ['usage', 'install', '使用', '安装', 'example', '示例'];
|
|
110
|
+
if (!docKeys.some(k => c.toLowerCase().includes(k)))
|
|
111
|
+
add('info', 'README.md 建议添加使用说明或示例', readme);
|
|
112
|
+
}
|
|
113
|
+
const design = path.join(modulePath, 'DESIGN.md');
|
|
114
|
+
if (fs.existsSync(design)) {
|
|
115
|
+
const c = fs.readFileSync(design, 'utf-8');
|
|
116
|
+
const designKeys = ['决策', 'decision', '选择', 'choice', '权衡', 'trade'];
|
|
117
|
+
if (!designKeys.some(k => c.toLowerCase().includes(k)))
|
|
118
|
+
add('info', 'DESIGN.md 建议记录设计决策和权衡', design);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { modulePath, issues, structure };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function formatStructure(s, indent = 0) {
|
|
125
|
+
const pre = ' '.repeat(indent);
|
|
126
|
+
if (s.type === 'dir') {
|
|
127
|
+
const lines = [`${pre}\u{1F4C1} ${s.name}/`];
|
|
128
|
+
for (const ch of (s.children || [])) lines.push(formatStructure(ch, indent + 1));
|
|
129
|
+
return lines.join('\n');
|
|
130
|
+
}
|
|
131
|
+
const sz = (s.size || 0) < 1024 ? `(${s.size} B)` : `(${Math.floor(s.size / 1024)} KB)`;
|
|
132
|
+
return `${pre}\u{1F4C4} ${s.name} ${sz}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function formatReport(r, verbose) {
|
|
136
|
+
const errs = r.issues.filter(i => i.severity === 'error').length;
|
|
137
|
+
const warns = r.issues.filter(i => i.severity === 'warning').length;
|
|
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);
|
|
149
|
+
if (verbose && r.structure.name) {
|
|
150
|
+
report += '\n' + '-'.repeat(40) + '\n目录结构:\n' + '-'.repeat(40) + '\n' + formatStructure(r.structure);
|
|
151
|
+
}
|
|
152
|
+
return report;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// CLI
|
|
156
|
+
const opts = parseCliArgs(process.argv);
|
|
157
|
+
const result = scanModule(opts.target);
|
|
158
|
+
const passed = !hasFatal(result.issues);
|
|
159
|
+
|
|
160
|
+
if (opts.json) {
|
|
161
|
+
console.log(JSON.stringify({
|
|
162
|
+
module_path: result.modulePath, passed,
|
|
163
|
+
error_count: result.issues.filter(i => i.severity === 'error').length,
|
|
164
|
+
warning_count: result.issues.filter(i => i.severity === 'warning').length,
|
|
165
|
+
issues: result.issues
|
|
166
|
+
}, null, 2));
|
|
167
|
+
} else {
|
|
168
|
+
console.log(formatReport(result, opts.verbose));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
process.exit(passed ? 0 : 1);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: verify-quality
|
|
3
3
|
description: 代码质量校验关卡。检测复杂度、重复代码、命名规范、函数长度等质量指标。当魔尊提到代码质量、复杂度检查、代码异味、重构建议、lint检查、代码规范时使用。在复杂模块、重构完成时自动触发。
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility: node>=18
|
|
4
6
|
user-invocable: true
|
|
5
7
|
disable-model-invocation: false
|
|
6
8
|
allowed-tools: Bash, Read, Glob
|
|
@@ -24,9 +26,9 @@ argument-hint: <扫描路径>
|
|
|
24
26
|
|
|
25
27
|
```bash
|
|
26
28
|
# 在 skill 目录下运行
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
node scripts/quality_checker.js <扫描路径>
|
|
30
|
+
node scripts/quality_checker.js <扫描路径> -v # 详细模式
|
|
31
|
+
node scripts/quality_checker.js <扫描路径> --json # JSON 输出
|
|
30
32
|
```
|
|
31
33
|
|
|
32
34
|
## 检测指标
|