devassist-agent 1.0.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 (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +467 -0
  3. package/bin/devassist.js +220 -0
  4. package/package.json +44 -0
  5. package/src/ai/adapter.js +464 -0
  6. package/src/ai/providers/claude.js +80 -0
  7. package/src/ai/providers/hunyuan.js +87 -0
  8. package/src/ai/providers/openai.js +74 -0
  9. package/src/ai/providers/qwen.js +81 -0
  10. package/src/cli/commands/ai.js +944 -0
  11. package/src/cli/commands/ask.js +79 -0
  12. package/src/cli/commands/backup.js +30 -0
  13. package/src/cli/commands/check.js +327 -0
  14. package/src/cli/commands/clean.js +130 -0
  15. package/src/cli/commands/comparison-report.js +326 -0
  16. package/src/cli/commands/convention.js +91 -0
  17. package/src/cli/commands/debt.js +49 -0
  18. package/src/cli/commands/deploy.js +88 -0
  19. package/src/cli/commands/diff.js +193 -0
  20. package/src/cli/commands/doctor.js +186 -0
  21. package/src/cli/commands/fix.js +195 -0
  22. package/src/cli/commands/init.js +431 -0
  23. package/src/cli/commands/inject.js +254 -0
  24. package/src/cli/commands/report.js +310 -0
  25. package/src/cli/commands/restore.js +78 -0
  26. package/src/cli/commands/schema.js +93 -0
  27. package/src/cli/commands/watch.js +212 -0
  28. package/src/cli/shared/code-context.js +51 -0
  29. package/src/cli/shared/config-loader.js +89 -0
  30. package/src/cli/shared/file-collector.js +116 -0
  31. package/src/cli/shared/inline-ignore.js +142 -0
  32. package/src/cli/shared/watch-list.js +281 -0
  33. package/src/core/event-bus.js +83 -0
  34. package/src/core/fsm.js +103 -0
  35. package/src/core/logger.js +54 -0
  36. package/src/core/rule-engine.js +117 -0
  37. package/src/index.js +64 -0
  38. package/src/modules/dev-time/arch-risk-assessor.js +250 -0
  39. package/src/modules/dev-time/code-quality-guard.js +1340 -0
  40. package/src/modules/dev-time/convention-store.js +201 -0
  41. package/src/modules/dev-time/impact-analyzer.js +292 -0
  42. package/src/modules/dev-time/pre-deploy-guard.js +284 -0
  43. package/src/modules/dev-time/schema-registry.js +284 -0
  44. package/src/modules/dev-time/tech-debt-tracker.js +225 -0
  45. package/src/modules/dev-time/version-manager.js +280 -0
  46. package/src/modules/runtime/channel-agent.js +404 -0
  47. package/src/modules/runtime/channel-middleware.js +316 -0
  48. package/src/modules/runtime/index.js +64 -0
  49. package/src/modules/runtime/infrastructure-guard.js +582 -0
  50. package/src/modules/runtime/notification-center.js +443 -0
  51. package/src/modules/runtime/schema-gatekeeper.js +664 -0
  52. package/src/modules/runtime/tool-registry.js +329 -0
  53. package/templates/ci/github-actions.yml +60 -0
  54. package/templates/ci/gitlab-ci.yml +30 -0
  55. package/templates/ci/pre-commit-hook.sh +18 -0
  56. package/templates/git-hooks/pre-commit +61 -0
  57. package/tests/run-all.js +434 -0
  58. package/tests/test-layer2.js +461 -0
  59. package/tests/test-new-rules.js +157 -0
@@ -0,0 +1,250 @@
1
+ /**
2
+ * ArchRiskAssessor - architecture risk evaluation.
3
+ *
4
+ * Defense target: "Architecture design flaws" (20% of 88 failures).
5
+ * #7 - RecoveryEngine infinite loop (347 lines to solve a rare problem, introduced 4 new bugs)
6
+ * #8 - duplicate function declarations
7
+ * #9 - over-engineering (347 lines for a problem that needed 20)
8
+ *
9
+ * This module:
10
+ * 1. Detects error-recovery loops that could become infinite
11
+ * 2. Measures complexity vs problem size (over-engineering detection)
12
+ * 3. Detects circular dependencies
13
+ * 4. Flags remote code injection risks
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const { Logger } = require('../../core/logger');
19
+ const log = new Logger('ArchRiskAssessor');
20
+
21
+ class ArchRiskAssessor {
22
+ constructor(projectRoot) {
23
+ this.projectRoot = projectRoot;
24
+ }
25
+
26
+ /**
27
+ * Analyze a single file for architecture risks.
28
+ */
29
+ analyzeFile(filePath, content) {
30
+ const findings = [];
31
+
32
+ if (!content) {
33
+ try { content = fs.readFileSync(filePath, 'utf-8'); } catch (_) { return findings; }
34
+ }
35
+
36
+ const lines = content.split('\n');
37
+ const lineCount = lines.length;
38
+
39
+ // --- Rule 1: Over-engineering detection ---
40
+ // If a single function/file is very large but solves a "simple" problem,
41
+ // flag it. The RecoveryEngine was 347 lines for a rarely-occurring issue.
42
+ const functionBlocks = this._extractFunctions(content);
43
+ for (const fn of functionBlocks) {
44
+ if (fn.lineCount > 150) {
45
+ findings.push({
46
+ ruleId: 'ar-over-engineering',
47
+ severity: 'warn',
48
+ category: 'arch',
49
+ message: `函数 '${fn.name}' 长达 ${fn.lineCount} 行,请评估此复杂度是否合理。(RecoveryEngine 曾 347 行,引入了 4 个新 bug。)`,
50
+ file: filePath,
51
+ line: fn.startLine,
52
+ suggestion: `拆分为更小的函数。如果是错误恢复逻辑,请确认不会进入无限循环。`,
53
+ context: { functionName: fn.name, lineCount: fn.lineCount },
54
+ });
55
+ }
56
+ }
57
+
58
+ // --- Rule 2: Error recovery loop detection ---
59
+ // Detects functions that call themselves inside a catch block (direct recursion in error recovery).
60
+ // Only flags DIRECT self-invocation: function foo() { try {} catch(e) { foo(); } }
61
+
62
+ // Pattern 1: explicit retry/recover/restart keywords in catch blocks
63
+ const retryRegex = /catch\s*\([^)]*\)\s*\{[^}]*?(?:retry|recover|restart|reinit|reload)\s*\(/gi;
64
+ let retryMatch;
65
+ while ((retryMatch = retryRegex.exec(content)) !== null) {
66
+ const line = content.substring(0, retryMatch.index).split('\n').length;
67
+ findings.push({
68
+ ruleId: 'ar-recovery-loop',
69
+ severity: 'block',
70
+ category: 'arch',
71
+ message: `错误恢复模式:在 catch 块中调用了 retry/recover/restart。这导致了故障 #7(RecoveryEngine 无限循环)。`,
72
+ file: filePath,
73
+ line,
74
+ suggestion: `添加重试计数器或最大重试次数限制。错误恢复代码中绝不允许无限制的重试循环。`,
75
+ context: { match: retryMatch[0].substring(0, 100) },
76
+ });
77
+ }
78
+
79
+ // Pattern 2: direct self-recursion — function foo() { ... catch(e) { foo( ... } }
80
+ const funcDeclRegex = /function\s+(\w+)\s*\([^)]*\)\s*\{/g;
81
+ let funcMatch;
82
+ while ((funcMatch = funcDeclRegex.exec(content)) !== null) {
83
+ const fnName = funcMatch[1];
84
+ const fnStart = funcMatch.index + funcMatch[0].length;
85
+ // Find the function body by counting braces from the opening {
86
+ let braceCount = 1;
87
+ let fnEnd = fnStart;
88
+ for (let i = fnStart; i < content.length; i++) {
89
+ if (content[i] === '{') braceCount++;
90
+ if (content[i] === '}') braceCount--;
91
+ if (braceCount === 0) { fnEnd = i; break; }
92
+ }
93
+ const fnBody = content.substring(fnStart, fnEnd);
94
+ // Check if this function calls itself inside a catch block
95
+ const catchRegex = new RegExp(`catch\\s*\\([^)]*\\)\\s*\\{[^}]*?\\b${fnName}\\s*\\(`);
96
+ if (catchRegex.test(fnBody)) {
97
+ const line = content.substring(0, funcMatch.index).split('\n').length;
98
+ findings.push({
99
+ ruleId: 'ar-recovery-loop',
100
+ severity: 'block',
101
+ category: 'arch',
102
+ message: `错误恢复循环:函数 '${fnName}' 在 catch 块中调用自身。这导致了故障 #7(RecoveryEngine 无限循环)。`,
103
+ file: filePath,
104
+ line,
105
+ suggestion: `添加重试计数器或最大重试次数限制。错误恢复代码中绝不允许无限制的重试循环。`,
106
+ context: { functionName: fnName },
107
+ });
108
+ }
109
+ }
110
+
111
+ // --- Rule 3: Circular dependency detection (requires graph) ---
112
+ // Handled at the project level in analyzeProject()
113
+
114
+ // --- Rule 4: Remote code injection risk ---
115
+ const injectionPatterns = [
116
+ { regex: /eval\s*\(\s*[^)]*(?:req|request|body|query|params)/gi, name: 'eval 使用了请求数据' },
117
+ { regex: /new\s+Function\s*\(\s*[^)]*(?:req|request|body|query)/gi, name: 'Function 构造函数使用了请求数据' },
118
+ { regex: /setTimeout\s*\(\s*[^,)]*(?:req|request|body|query)[^,)]*\s*,/gi, name: 'setTimeout 字符串求值使用了请求数据' },
119
+ { regex: /innerHTML\s*=\s*[^;]*(?:req|request|body|query|response)/gi, name: 'innerHTML 使用了不可信数据(XSS 风险)' },
120
+ ];
121
+ for (const { regex, name } of injectionPatterns) {
122
+ let match;
123
+ while ((match = regex.exec(content)) !== null) {
124
+ const line = content.substring(0, match.index).split('\n').length;
125
+ findings.push({
126
+ ruleId: 'ar-code-injection',
127
+ severity: 'block',
128
+ category: 'arch',
129
+ message: `潜在代码注入:${name}`,
130
+ file: filePath,
131
+ line,
132
+ suggestion: `切勿将不可信输入传给 eval/Function/innerHTML。对所有外部数据进行消毒和校验。`,
133
+ context: { match: match[0].substring(0, 100) },
134
+ });
135
+ }
136
+ }
137
+
138
+ // --- Rule 5: Cyclomatic complexity estimation ---
139
+ const complexity = this._estimateComplexity(content);
140
+ if (complexity > 20) {
141
+ findings.push({
142
+ ruleId: 'ar-high-complexity',
143
+ severity: 'warn',
144
+ category: 'arch',
145
+ message: `圈复杂度过高(约 ${complexity}),阈值为 20。`,
146
+ file: filePath,
147
+ line: 1,
148
+ suggestion: `重构以降低分支复杂度。将条件提取为具名辅助函数,或使用提前返回。`,
149
+ context: { complexity },
150
+ });
151
+ }
152
+
153
+ return findings;
154
+ }
155
+
156
+ /**
157
+ * Analyze the entire project for circular dependencies.
158
+ */
159
+ analyzeProject(dependencyGraph) {
160
+ const findings = [];
161
+
162
+ if (!dependencyGraph || dependencyGraph.size === 0) {
163
+ return findings;
164
+ }
165
+
166
+ // Detect circular dependencies using DFS
167
+ const visited = new Set();
168
+ const recursionStack = new Set();
169
+
170
+ const detectCycle = (node, path) => {
171
+ if (recursionStack.has(node)) {
172
+ // Found a cycle
173
+ const cycleStart = path.indexOf(node);
174
+ const cycle = path.slice(cycleStart).concat(node);
175
+ findings.push({
176
+ ruleId: 'ar-circular-dependency',
177
+ severity: 'block',
178
+ category: 'arch',
179
+ message: `检测到循环依赖:${cycle.join(' → ')}`,
180
+ file: node,
181
+ suggestion: `通过将共享逻辑提取到独立模块来打破循环,或使用依赖注入。`,
182
+ context: { cycle },
183
+ });
184
+ return;
185
+ }
186
+ if (visited.has(node)) return;
187
+
188
+ visited.add(node);
189
+ recursionStack.add(node);
190
+
191
+ const node_data = dependencyGraph.get(node);
192
+ if (node_data && node_data.imports) {
193
+ for (const imp of node_data.imports) {
194
+ detectCycle(imp, [...path, node]);
195
+ }
196
+ }
197
+
198
+ recursionStack.delete(node);
199
+ };
200
+
201
+ for (const node of dependencyGraph.keys()) {
202
+ visited.clear();
203
+ recursionStack.clear();
204
+ detectCycle(node, []);
205
+ }
206
+
207
+ return findings;
208
+ }
209
+
210
+ _extractFunctions(content) {
211
+ const functions = [];
212
+ const regex = /(?:function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:function|\([^)]*\)\s*=>))\s*[\({]/g;
213
+ let match;
214
+ while ((match = regex.exec(content)) !== null) {
215
+ const name = match[1] || match[2];
216
+ const startLine = content.substring(0, match.index).split('\n').length;
217
+
218
+ // Find the end of the function by counting braces
219
+ let braceCount = 0;
220
+ let started = false;
221
+ let endLine = startLine;
222
+ const lines = content.substring(match.index).split('\n');
223
+ for (let i = 0; i < lines.length; i++) {
224
+ for (const ch of lines[i]) {
225
+ if (ch === '{') { braceCount++; started = true; }
226
+ if (ch === '}') braceCount--;
227
+ if (started && braceCount === 0) { endLine = startLine + i; break; }
228
+ }
229
+ if (started && braceCount === 0) break;
230
+ }
231
+ functions.push({ name, startLine, lineCount: endLine - startLine + 1 });
232
+ }
233
+ return functions;
234
+ }
235
+
236
+ _estimateComplexity(content) {
237
+ // Simplified cyclomatic complexity: count decision points
238
+ const decisionPoints = (content.match(/\bif\s*\(/g) || []).length
239
+ + (content.match(/\belse\s+if\s*\(/g) || []).length
240
+ + (content.match(/\bfor\s*\(/g) || []).length
241
+ + (content.match(/\bwhile\s*\(/g) || []).length
242
+ + (content.match(/\bcase\s+/g) || []).length
243
+ + (content.match(/&&/g) || []).length
244
+ + (content.match(/\|\|/g) || []).length
245
+ + (content.match(/\?[^:]*:/g) || []).length;
246
+ return decisionPoints + 1;
247
+ }
248
+ }
249
+
250
+ module.exports = { ArchRiskAssessor };