rules-enforcer 1.0.0 → 2.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 (2) hide show
  1. package/package.json +8 -4
  2. package/rules-server.js +1066 -350
package/rules-server.js CHANGED
@@ -1,350 +1,1066 @@
1
- const readline = require('readline');
2
- const { stdin, stdout } = require('process');
3
-
4
- const l1Rules = {
5
- codeQuality: {
6
- name: "代码质量检查",
7
- description: "TypeScript严格模式、ESLint检查",
8
- validate: (code) => {
9
- const issues = [];
10
- if (code.includes('var ')) {
11
- issues.push({ rule: 'L1-001', message: '使用var声明变量,应使用const或let', severity: 'warning' });
12
- }
13
- if (code.includes('console.log(')) {
14
- issues.push({ rule: 'L1-002', message: '存在console.log,生产环境应移除', severity: 'info' });
15
- }
16
- return issues;
17
- }
18
- },
19
- simplicity: {
20
- name: "简约原则检查",
21
- description: "代码应简洁明了",
22
- validate: (code) => {
23
- const issues = [];
24
- const lines = code.split('\n');
25
- for (let i = 0; i < lines.length; i++) {
26
- if (lines[i].length > 120) {
27
- issues.push({ rule: 'L1-003', message: `第${i + 1}行超过120字符`, severity: 'warning' });
28
- }
29
- }
30
- if (lines.length > 50) {
31
- issues.push({ rule: 'L1-004', message: '函数超过50行,建议拆分', severity: 'info' });
32
- }
33
- return issues;
34
- }
35
- }
36
- };
37
-
38
- const l2Rules = {
39
- singleResponsibility: {
40
- name: "单一职责检查",
41
- description: "每个类/函数只负责一件事",
42
- validate: (code) => {
43
- const issues = [];
44
- const functionMatches = code.match(/function\s+\w+\s*\([^)]*\)\s*\{/g);
45
- if (functionMatches) {
46
- functionMatches.forEach((func) => {
47
- const funcName = func.match(/function\s+(\w+)/)[1];
48
- if (funcName.includes('And') || funcName.includes('Or')) {
49
- issues.push({ rule: 'L2-001', message: `函数${funcName}可能违反单一职责原则`, severity: 'warning' });
50
- }
51
- });
52
- }
53
- return issues;
54
- }
55
- },
56
- directoryStructure: {
57
- name: "目录结构检查",
58
- description: "符合项目目录结构规范",
59
- validate: (code, context) => {
60
- const issues = [];
61
- if (context && context.filePath) {
62
- const path = context.filePath.toLowerCase();
63
- if (path.includes('controllers') && !path.endsWith('.js') && !path.endsWith('.ts')) {
64
- issues.push({ rule: 'L2-002', message: `控制器文件应使用.js或.ts扩展名`, severity: 'error' });
65
- }
66
- if (path.includes('routes') && !path.endsWith('.js') && !path.endsWith('.ts')) {
67
- issues.push({ rule: 'L2-003', message: `路由文件应使用.js或.ts扩展名`, severity: 'error' });
68
- }
69
- }
70
- return issues;
71
- }
72
- },
73
- namingConvention: {
74
- name: "命名规范检查",
75
- description: "符合项目命名规范",
76
- validate: (code) => {
77
- const issues = [];
78
- const camelCaseRegex = /\b[A-Z][a-zA-Z0-9]*\b/g;
79
- const matches = code.match(camelCaseRegex);
80
- if (matches) {
81
- matches.forEach(match => {
82
- if (match.length > 1 && !['class', 'function', 'const', 'let', 'var', 'if', 'else', 'for', 'while', 'return', 'import', 'export', 'from', 'async', 'await', 'new', 'this', 'try', 'catch', 'throw'].includes(match.toLowerCase())) {
83
- if (!/^[A-Z]/.test(match)) {
84
- issues.push({ rule: 'L2-004', message: `类名${match}应使用PascalCase`, severity: 'warning' });
85
- }
86
- }
87
- });
88
- }
89
- return issues;
90
- }
91
- }
92
- };
93
-
94
- const l3Rules = {
95
- dry: {
96
- name: "DRY原则检查",
97
- description: "避免重复代码",
98
- validate: (code) => {
99
- const issues = [];
100
- const lines = code.split('\n');
101
- const lineCounts = {};
102
- lines.forEach(line => {
103
- const trimmed = line.trim();
104
- if (trimmed && trimmed.length > 20) {
105
- lineCounts[trimmed] = (lineCounts[trimmed] || 0) + 1;
106
- }
107
- });
108
- Object.entries(lineCounts).forEach(([line, count]) => {
109
- if (count >= 3) {
110
- issues.push({ rule: 'L3-001', message: `发现重复代码片段,出现${count}次,应提取为函数`, severity: 'warning' });
111
- }
112
- });
113
- return issues;
114
- }
115
- },
116
- solid: {
117
- name: "SOLID原则检查",
118
- description: "符合SOLID设计原则",
119
- validate: (code) => {
120
- const issues = [];
121
- if (code.includes('extends') && code.includes('implements')) {
122
- issues.push({ rule: 'L3-002', message: '同时使用extends和implements,建议检查是否符合里氏替换原则', severity: 'info' });
123
- }
124
- return issues;
125
- }
126
- },
127
- dataStructure: {
128
- name: "数据结构优先检查",
129
- description: "使用合适的数据结构",
130
- validate: (code) => {
131
- const issues = [];
132
- if (code.includes('for (let i = 0; i < ') && code.includes('.length')) {
133
- issues.push({ rule: 'L3-003', message: '使用普通for循环,考虑使用forEach或map', severity: 'info' });
134
- }
135
- return issues;
136
- }
137
- }
138
- };
139
-
140
- function validateCode(code, context = {}) {
141
- const results = [];
142
- Object.values(l1Rules).forEach(rule => {
143
- const issues = rule.validate(code, context);
144
- issues.forEach(issue => {
145
- results.push({ ...issue, level: 'L1', ruleName: rule.name });
146
- });
147
- });
148
- Object.values(l2Rules).forEach(rule => {
149
- const issues = rule.validate(code, context);
150
- issues.forEach(issue => {
151
- results.push({ ...issue, level: 'L2', ruleName: rule.name });
152
- });
153
- });
154
- Object.values(l3Rules).forEach(rule => {
155
- const issues = rule.validate(code, context);
156
- issues.forEach(issue => {
157
- results.push({ ...issue, level: 'L3', ruleName: rule.name });
158
- });
159
- });
160
- return results;
161
- }
162
-
163
- function getRules() {
164
- return {
165
- L1: Object.keys(l1Rules).map(key => ({
166
- id: key,
167
- name: l1Rules[key].name,
168
- description: l1Rules[key].description
169
- })),
170
- L2: Object.keys(l2Rules).map(key => ({
171
- id: key,
172
- name: l2Rules[key].name,
173
- description: l2Rules[key].description
174
- })),
175
- L3: Object.keys(l3Rules).map(key => ({
176
- id: key,
177
- name: l3Rules[key].name,
178
- description: l3Rules[key].description
179
- }))
180
- };
181
- }
182
-
183
- const rl = readline.createInterface({
184
- input: stdin,
185
- output: stdout,
186
- terminal: false
187
- });
188
-
189
- rl.on('line', (input) => {
190
- try {
191
- const request = JSON.parse(input);
192
- const { id, method, params } = request;
193
- let response;
194
-
195
- if (method === 'initialize') {
196
- response = {
197
- jsonrpc: "2.0",
198
- id: id,
199
- result: {
200
- protocolVersion: "2024-11-05",
201
- capabilities: {
202
- tools: {}
203
- },
204
- serverInfo: {
205
- name: "rules-enforcer",
206
- version: "1.0.0"
207
- }
208
- }
209
- };
210
- } else if (method === 'notifications/initialized') {
211
- return;
212
- } else if (method === 'tools/list') {
213
- response = {
214
- jsonrpc: "2.0",
215
- id: id,
216
- result: {
217
- tools: [
218
- {
219
- name: "validate",
220
- description: "验证代码是否符合L1/L2/L3规则",
221
- inputSchema: {
222
- type: "object",
223
- properties: {
224
- code: { type: "string", description: "要验证的代码" },
225
- context: { type: "object", description: "上下文信息" }
226
- },
227
- required: ["code"]
228
- }
229
- },
230
- {
231
- name: "get_rules",
232
- description: "获取所有规则定义",
233
- inputSchema: {
234
- type: "object",
235
- properties: {}
236
- }
237
- },
238
- {
239
- name: "enforce",
240
- description: "强制执行规则验证,不通过则拒绝",
241
- inputSchema: {
242
- type: "object",
243
- properties: {
244
- code: { type: "string", description: "要验证的代码" },
245
- context: { type: "object", description: "上下文信息" }
246
- },
247
- required: ["code"]
248
- }
249
- }
250
- ]
251
- }
252
- };
253
- } else if (method === 'tools/call') {
254
- const toolName = params?.name;
255
- const toolArgs = params?.arguments || {};
256
-
257
- if (toolName === 'validate') {
258
- const issues = validateCode(toolArgs.code, toolArgs.context);
259
- response = {
260
- jsonrpc: "2.0",
261
- id: id,
262
- result: {
263
- content: [
264
- {
265
- type: "text",
266
- text: JSON.stringify({
267
- success: true,
268
- issues,
269
- summary: {
270
- total: issues.length,
271
- errors: issues.filter(i => i.severity === 'error').length,
272
- warnings: issues.filter(i => i.severity === 'warning').length,
273
- info: issues.filter(i => i.severity === 'info').length
274
- }
275
- })
276
- }
277
- ]
278
- }
279
- };
280
- } else if (toolName === 'get_rules') {
281
- response = {
282
- jsonrpc: "2.0",
283
- id: id,
284
- result: {
285
- content: [
286
- {
287
- type: "text",
288
- text: JSON.stringify({
289
- success: true,
290
- rules: getRules()
291
- })
292
- }
293
- ]
294
- }
295
- };
296
- } else if (toolName === 'enforce') {
297
- const issues = validateCode(toolArgs.code, toolArgs.context);
298
- const errors = issues.filter(i => i.severity === 'error');
299
- const status = errors.length === 0 ? 'accepted' : 'rejected';
300
- response = {
301
- jsonrpc: "2.0",
302
- id: id,
303
- result: {
304
- content: [
305
- {
306
- type: "text",
307
- text: JSON.stringify({
308
- success: true,
309
- status,
310
- issues,
311
- errors
312
- })
313
- }
314
- ]
315
- }
316
- };
317
- } else {
318
- response = {
319
- jsonrpc: "2.0",
320
- id: id,
321
- error: {
322
- code: -32601,
323
- message: 'Tool not found'
324
- }
325
- };
326
- }
327
- } else {
328
- response = {
329
- jsonrpc: "2.0",
330
- id: id,
331
- error: {
332
- code: -32601,
333
- message: 'Method not found'
334
- }
335
- };
336
- }
337
-
338
- stdout.write(JSON.stringify(response) + '\n');
339
- } catch (error) {
340
- console.error('Error:', error.message);
341
- stdout.write(JSON.stringify({
342
- jsonrpc: "2.0",
343
- id: null,
344
- error: {
345
- code: -32603,
346
- message: 'Internal error'
347
- }
348
- }) + '\n');
349
- }
350
- });
1
+ /**
2
+ * Rules Enforcer MCP Server - 整合版
3
+ *
4
+ * 整合内容:
5
+ * - 原有功能:L1/L2/L3 规则验证(validate、get_rules、enforce)
6
+ * - 新增功能:项目扫描、技术栈识别、目录拓扑、组件检测(来自 project-detector 插件)
7
+ *
8
+ * 修改记录:
9
+ * - 2026-06-06: 整合 project-detector 插件功能到 MCP
10
+ */
11
+
12
+ const readline = require('readline');
13
+ const { stdin, stdout } = require('process');
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+
17
+ // ============================================================
18
+ // 第一部分:原有规则验证功能(保留不变)
19
+ // ============================================================
20
+
21
+ const l1Rules = {
22
+ codeQuality: {
23
+ name: "代码质量检查",
24
+ description: "TypeScript严格模式、ESLint检查",
25
+ validate: (code) => {
26
+ const issues = [];
27
+ if (code.includes('var ')) {
28
+ issues.push({ rule: 'L1-001', message: '使用var声明变量,应使用const或let', severity: 'warning' });
29
+ }
30
+ if (code.includes('console.log(')) {
31
+ issues.push({ rule: 'L1-002', message: '存在console.log,生产环境应移除', severity: 'info' });
32
+ }
33
+ return issues;
34
+ }
35
+ },
36
+ simplicity: {
37
+ name: "简约原则检查",
38
+ description: "代码应简洁明了",
39
+ validate: (code) => {
40
+ const issues = [];
41
+ const lines = code.split('\n');
42
+ for (let i = 0; i < lines.length; i++) {
43
+ if (lines[i].length > 120) {
44
+ issues.push({ rule: 'L1-003', message: `第${i + 1}行超过120字符`, severity: 'warning' });
45
+ }
46
+ }
47
+ if (lines.length > 50) {
48
+ issues.push({ rule: 'L1-004', message: '函数超过50行,建议拆分', severity: 'info' });
49
+ }
50
+ return issues;
51
+ }
52
+ }
53
+ };
54
+
55
+ const l2Rules = {
56
+ singleResponsibility: {
57
+ name: "单一职责检查",
58
+ description: "每个类/函数只负责一件事",
59
+ validate: (code) => {
60
+ const issues = [];
61
+ const functionMatches = code.match(/function\s+\w+\s*\([^)]*\)\s*\{/g);
62
+ if (functionMatches) {
63
+ functionMatches.forEach((func) => {
64
+ const funcName = func.match(/function\s+(\w+)/)[1];
65
+ if (funcName.includes('And') || funcName.includes('Or')) {
66
+ issues.push({ rule: 'L2-001', message: `函数${funcName}可能违反单一职责原则`, severity: 'warning' });
67
+ }
68
+ });
69
+ }
70
+ return issues;
71
+ }
72
+ },
73
+ directoryStructure: {
74
+ name: "目录结构检查",
75
+ description: "符合项目目录结构规范",
76
+ validate: (code, context) => {
77
+ const issues = [];
78
+ if (context && context.filePath) {
79
+ const filePath = context.filePath.toLowerCase();
80
+ if (filePath.includes('controllers') && !filePath.endsWith('.js') && !filePath.endsWith('.ts')) {
81
+ issues.push({ rule: 'L2-002', message: `控制器文件应使用.js或.ts扩展名`, severity: 'error' });
82
+ }
83
+ if (filePath.includes('routes') && !filePath.endsWith('.js') && !filePath.endsWith('.ts')) {
84
+ issues.push({ rule: 'L2-003', message: `路由文件应使用.js或.ts扩展名`, severity: 'error' });
85
+ }
86
+ }
87
+ return issues;
88
+ }
89
+ },
90
+ namingConvention: {
91
+ name: "命名规范检查",
92
+ description: "符合项目命名规范",
93
+ validate: (code) => {
94
+ const issues = [];
95
+ const camelCaseRegex = /\b[A-Z][a-zA-Z0-9]*\b/g;
96
+ const matches = code.match(camelCaseRegex);
97
+ if (matches) {
98
+ matches.forEach(match => {
99
+ if (match.length > 1 && !['class', 'function', 'const', 'let', 'var', 'if', 'else', 'for', 'while', 'return', 'import', 'export', 'from', 'async', 'await', 'new', 'this', 'try', 'catch', 'throw'].includes(match.toLowerCase())) {
100
+ if (!/^[A-Z]/.test(match)) {
101
+ issues.push({ rule: 'L2-004', message: `类名${match}应使用PascalCase`, severity: 'warning' });
102
+ }
103
+ }
104
+ });
105
+ }
106
+ return issues;
107
+ }
108
+ }
109
+ };
110
+
111
+ const l3Rules = {
112
+ dry: {
113
+ name: "DRY原则检查",
114
+ description: "避免重复代码",
115
+ validate: (code) => {
116
+ const issues = [];
117
+ const lines = code.split('\n');
118
+ const lineCounts = {};
119
+ lines.forEach(line => {
120
+ const trimmed = line.trim();
121
+ if (trimmed && trimmed.length > 20) {
122
+ lineCounts[trimmed] = (lineCounts[trimmed] || 0) + 1;
123
+ }
124
+ });
125
+ Object.entries(lineCounts).forEach(([line, count]) => {
126
+ if (count >= 3) {
127
+ issues.push({ rule: 'L3-001', message: `发现重复代码片段,出现${count}次,应提取为函数`, severity: 'warning' });
128
+ }
129
+ });
130
+ return issues;
131
+ }
132
+ },
133
+ solid: {
134
+ name: "SOLID原则检查",
135
+ description: "符合SOLID设计原则",
136
+ validate: (code) => {
137
+ const issues = [];
138
+ if (code.includes('extends') && code.includes('implements')) {
139
+ issues.push({ rule: 'L3-002', message: '同时使用extends和implements,建议检查是否符合里氏替换原则', severity: 'info' });
140
+ }
141
+ return issues;
142
+ }
143
+ },
144
+ dataStructure: {
145
+ name: "数据结构优先检查",
146
+ description: "使用合适的数据结构",
147
+ validate: (code) => {
148
+ const issues = [];
149
+ if (code.includes('for (let i = 0; i < ') && code.includes('.length')) {
150
+ issues.push({ rule: 'L3-003', message: '使用普通for循环,考虑使用forEach或map', severity: 'info' });
151
+ }
152
+ return issues;
153
+ }
154
+ }
155
+ };
156
+
157
+ function validateCode(code, context = {}) {
158
+ const results = [];
159
+ Object.values(l1Rules).forEach(rule => {
160
+ const issues = rule.validate(code, context);
161
+ issues.forEach(issue => {
162
+ results.push({ ...issue, level: 'L1', ruleName: rule.name });
163
+ });
164
+ });
165
+ Object.values(l2Rules).forEach(rule => {
166
+ const issues = rule.validate(code, context);
167
+ issues.forEach(issue => {
168
+ results.push({ ...issue, level: 'L2', ruleName: rule.name });
169
+ });
170
+ });
171
+ Object.values(l3Rules).forEach(rule => {
172
+ const issues = rule.validate(code, context);
173
+ issues.forEach(issue => {
174
+ results.push({ ...issue, level: 'L3', ruleName: rule.name });
175
+ });
176
+ });
177
+ return results;
178
+ }
179
+
180
+ function getRules() {
181
+ return {
182
+ L1: Object.keys(l1Rules).map(key => ({
183
+ id: key,
184
+ name: l1Rules[key].name,
185
+ description: l1Rules[key].description
186
+ })),
187
+ L2: Object.keys(l2Rules).map(key => ({
188
+ id: key,
189
+ name: l2Rules[key].name,
190
+ description: l2Rules[key].description
191
+ })),
192
+ L3: Object.keys(l3Rules).map(key => ({
193
+ id: key,
194
+ name: l3Rules[key].name,
195
+ description: l3Rules[key].description
196
+ }))
197
+ };
198
+ }
199
+
200
+ // ============================================================
201
+ // 第二部分:新增项目扫描功能(来自 project-detector 插件)
202
+ // ============================================================
203
+
204
+ /**
205
+ * ProjectDetector 类 - 整合自 project-detector 插件
206
+ * 功能:只读扫描项目结构,识别技术栈、目录拓扑、已有组件
207
+ */
208
+ class ProjectDetector {
209
+ constructor(projectRoot) {
210
+ if (!projectRoot || typeof projectRoot !== 'string') {
211
+ throw new Error('[ProjectDetector] 项目路径参数无效');
212
+ }
213
+
214
+ this.root = path.resolve(projectRoot);
215
+
216
+ if (!fs.existsSync(this.root) || !fs.statSync(this.root).isDirectory()) {
217
+ throw new Error(`[ProjectDetector] 项目路径不存在或不是目录: ${this.root}`);
218
+ }
219
+
220
+ this.techStack = [];
221
+ this.structure = null;
222
+ this.fullTopology = [];
223
+ this.existingComponents = {
224
+ mcp: [],
225
+ agents: [],
226
+ rules: []
227
+ };
228
+ this.fileErrors = [];
229
+ }
230
+
231
+ logFileError(filePath, error, context) {
232
+ this.fileErrors.push({
233
+ file: filePath,
234
+ error: String(error),
235
+ context: context,
236
+ timestamp: new Date().toISOString()
237
+ });
238
+ }
239
+
240
+ scanTechStack() {
241
+ const checks = [
242
+ { file: 'package.json', detect: (content) => this.detectNodeJS(content) },
243
+ { file: 'tsconfig.json', detect: () => 'TypeScript' },
244
+ { file: 'requirements.txt', detect: () => 'Python' },
245
+ { file: 'go.mod', detect: () => 'Go' },
246
+ { file: 'Cargo.toml', detect: () => 'Rust' },
247
+ { file: 'pom.xml', detect: () => 'Maven/Java' }
248
+ ];
249
+
250
+ for (const check of checks) {
251
+ const filePath = path.join(this.root, check.file);
252
+ if (fs.existsSync(filePath)) {
253
+ try {
254
+ const content = fs.readFileSync(filePath, 'utf8');
255
+ const detected = check.detect(content);
256
+ if (detected) {
257
+ const stacks = Array.isArray(detected) ? detected : [detected];
258
+ for (const stack of stacks) {
259
+ if (stack && !this.techStack.includes(stack)) {
260
+ this.techStack.push(stack);
261
+ }
262
+ }
263
+ }
264
+ } catch (e) {
265
+ this.logFileError(check.file, e, 'scanTechStack');
266
+ }
267
+ }
268
+ }
269
+
270
+ return this.techStack;
271
+ }
272
+
273
+ detectNodeJS(pkgContent) {
274
+ try {
275
+ const pkg = JSON.parse(pkgContent);
276
+ const deps = pkg.dependencies || {};
277
+ const frameworks = [];
278
+
279
+ if (deps.express || deps['express-']) frameworks.push('Express.js');
280
+ if (deps.koa) frameworks.push('Koa');
281
+ if (deps.fastify) frameworks.push('Fastify');
282
+ if (deps.next) frameworks.push('Next.js');
283
+ if (deps.nuxt) frameworks.push('Nuxt.js');
284
+ if (deps.vite) frameworks.push('Vite');
285
+ if (deps.webpack) frameworks.push('Webpack');
286
+ if (deps.react) frameworks.push('React');
287
+ if (deps.vue) frameworks.push('Vue.js');
288
+ if (deps.sequelize) frameworks.push('Sequelize/ORM');
289
+ if (deps.typeorm) frameworks.push('TypeORM');
290
+ if (deps['better-sqlite3'] || deps.sqlite3) frameworks.push('SQLite');
291
+ if (deps.mongoose) frameworks.push('MongoDB/Mongoose');
292
+
293
+ return ['Node.js', ...frameworks];
294
+ } catch {
295
+ return ['Node.js'];
296
+ }
297
+ }
298
+
299
+ scanDirectoryStructure(maxDepth = 4) {
300
+ this.fullTopology = [];
301
+
302
+ try {
303
+ this.structure = this._scanDir(this.root, 0, maxDepth);
304
+ } catch (e) {
305
+ this.logFileError(this.root, e, 'scanDirectoryStructure');
306
+ this.structure = null;
307
+ }
308
+
309
+ return this.structure;
310
+ }
311
+
312
+ _scanDir(dir, depth, maxDepth) {
313
+ if (depth > maxDepth) return null;
314
+
315
+ const name = path.basename(dir);
316
+ const relativePath = path.relative(this.root, dir) || '.';
317
+
318
+ const node = {
319
+ name,
320
+ path: relativePath,
321
+ depth,
322
+ type: this.categorizeDir(name),
323
+ files: [],
324
+ dirs: []
325
+ };
326
+
327
+ this.fullTopology.push({
328
+ path: relativePath,
329
+ depth,
330
+ type: 'directory',
331
+ category: node.type
332
+ });
333
+
334
+ try {
335
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
336
+
337
+ for (const entry of entries) {
338
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') {
339
+ continue;
340
+ }
341
+
342
+ const fullPath = path.join(dir, entry.name);
343
+
344
+ if (entry.isDirectory()) {
345
+ const childNode = this._scanDir(fullPath, depth + 1, maxDepth);
346
+ if (childNode) {
347
+ node.dirs.push(childNode);
348
+ }
349
+ } else {
350
+ const fileInfo = {
351
+ name: entry.name,
352
+ path: path.relative(this.root, fullPath),
353
+ type: this.categorizeFile(entry.name)
354
+ };
355
+ node.files.push(fileInfo);
356
+
357
+ this.fullTopology.push({
358
+ path: fileInfo.path,
359
+ depth: depth + 1,
360
+ type: 'file',
361
+ category: fileInfo.type
362
+ });
363
+ }
364
+ }
365
+ } catch (e) {
366
+ this.logFileError(dir, e, `_scanDir depth=${depth}`);
367
+ }
368
+
369
+ return node;
370
+ }
371
+
372
+ categorizeDir(name) {
373
+ const categories = {
374
+ 'src': 'business_code',
375
+ 'lib': 'business_code',
376
+ 'app': 'business_code',
377
+ 'pages': 'frontend_pages',
378
+ 'components': 'frontend_components',
379
+ 'views': 'frontend_views',
380
+ 'test': 'test_directory',
381
+ 'tests': 'test_directory',
382
+ '__tests__': 'test_directory',
383
+ 'spec': 'test_directory',
384
+ 'scripts': 'deployment_scripts',
385
+ 'config': 'configuration',
386
+ 'configs': 'configuration',
387
+ 'docs': 'documentation',
388
+ 'public': 'static_assets',
389
+ 'static': 'static_assets',
390
+ 'dist': 'build_output',
391
+ 'build': 'build_output',
392
+ '.trae': 'trae_config',
393
+ '.claude': 'claude_config',
394
+ 'database': 'data_storage',
395
+ 'mcp': 'mcp_services',
396
+ 'rules': 'rules_engine',
397
+ 'utils': 'utilities',
398
+ 'middleware': 'middleware',
399
+ 'routes': 'routing'
400
+ };
401
+ return categories[name] || 'other';
402
+ }
403
+
404
+ categorizeFile(name) {
405
+ if (name.endsWith('.test.js') || name.endsWith('.spec.js')) return 'test_file';
406
+ if (name.endsWith('.test.ts') || name.endsWith('.spec.ts')) return 'test_file';
407
+ if (name === 'package.json') return 'dependency_manifest';
408
+ if (name === 'AGENTS.md' || name === 'CLAUDE.md') return 'trae_config';
409
+ if (name === 'user_rules.md') return 'rules_L1';
410
+ if (name === 'project_rules.md') return 'rules_L2';
411
+ if (name.endsWith('.yml') || name.endsWith('.yaml')) return 'config_yaml';
412
+ if (name.endsWith('.json')) return 'config_json';
413
+ if (name.endsWith('.js') || name.endsWith('.ts')) return 'source_code';
414
+ if (name.endsWith('.md')) return 'documentation';
415
+ return 'other';
416
+ }
417
+
418
+ scanMCP() {
419
+ const mcpConfigPaths = [
420
+ path.join(this.root, '.trae', 'mcp', 'mcp.json'),
421
+ path.join(this.root, '.trae', 'mcp.json'),
422
+ path.join(process.env.APPDATA || '', 'Trae', 'User', 'globalStorage', 'mcp.json')
423
+ ];
424
+
425
+ for (const configPath of mcpConfigPaths) {
426
+ if (fs.existsSync(configPath)) {
427
+ try {
428
+ const content = fs.readFileSync(configPath, 'utf8');
429
+ const config = JSON.parse(content);
430
+ if (config.mcpServers) {
431
+ this.existingComponents.mcp = Object.keys(config.mcpServers);
432
+ break;
433
+ }
434
+ } catch (e) {
435
+ this.logFileError(configPath, e, 'scanMCP');
436
+ }
437
+ }
438
+ }
439
+
440
+ return this.existingComponents.mcp;
441
+ }
442
+
443
+ scanAgents() {
444
+ const rootAgentsPath = path.join(this.root, 'AGENTS.md');
445
+ if (fs.existsSync(rootAgentsPath)) {
446
+ this.existingComponents.agents.push({
447
+ name: 'AGENTS.md',
448
+ path: 'AGENTS.md',
449
+ type: 'root_config'
450
+ });
451
+ }
452
+
453
+ const agentsDir = path.join(this.root, '.trae', 'agents');
454
+ if (fs.existsSync(agentsDir)) {
455
+ try {
456
+ const files = fs.readdirSync(agentsDir);
457
+ for (const file of files) {
458
+ if (file.endsWith('.md')) {
459
+ this.existingComponents.agents.push({
460
+ name: file,
461
+ path: path.join('.trae', 'agents', file),
462
+ type: 'sub_agent'
463
+ });
464
+ }
465
+ }
466
+ } catch (e) {
467
+ this.logFileError(agentsDir, e, 'scanAgents');
468
+ }
469
+ }
470
+
471
+ return this.existingComponents.agents;
472
+ }
473
+
474
+ scanRules() {
475
+ const rulesDir = path.join(this.root, '.trae', 'rules');
476
+
477
+ if (!fs.existsSync(rulesDir)) {
478
+ return this.existingComponents.rules;
479
+ }
480
+
481
+ const levelRules = [
482
+ { dir: 'L1', level: 'L1' },
483
+ { dir: 'L2', level: 'L2' }
484
+ ];
485
+
486
+ for (const rule of levelRules) {
487
+ const levelDir = path.join(rulesDir, rule.dir);
488
+ if (fs.existsSync(levelDir)) {
489
+ try {
490
+ const files = fs.readdirSync(levelDir);
491
+ for (const file of files) {
492
+ if (file.endsWith('.md')) {
493
+ this.existingComponents.rules.push({
494
+ name: file,
495
+ path: path.join('.trae', 'rules', rule.dir, file),
496
+ level: rule.level
497
+ });
498
+ }
499
+ }
500
+ } catch (e) {
501
+ this.logFileError(levelDir, e, `scanRules/${rule.dir}`);
502
+ }
503
+ }
504
+ }
505
+
506
+ const l3Dirs = ['L3/RZero', 'custom_config'];
507
+ for (const l3Dir of l3Dirs) {
508
+ const l3Path = path.join(rulesDir, l3Dir);
509
+ if (fs.existsSync(l3Path)) {
510
+ try {
511
+ const files = fs.readdirSync(l3Path);
512
+ for (const file of files) {
513
+ if (file.endsWith('.md') || file.endsWith('.yml') || file.endsWith('.yaml')) {
514
+ this.existingComponents.rules.push({
515
+ name: file,
516
+ path: path.join('.trae', 'rules', l3Dir, file),
517
+ level: 'L3',
518
+ module: l3Dir.includes('RZero') ? 'RZero' : 'custom_config'
519
+ });
520
+ }
521
+ }
522
+ } catch (e) {
523
+ this.logFileError(l3Path, e, `scanRules/${l3Dir}`);
524
+ }
525
+ }
526
+ }
527
+
528
+ const attnPath = path.join(rulesDir, 'attention_optim_rule.md');
529
+ if (fs.existsSync(attnPath)) {
530
+ this.existingComponents.rules.push({
531
+ name: 'attention_optim_rule.md',
532
+ path: path.join('.trae', 'rules', 'attention_optim_rule.md'),
533
+ level: 'custom',
534
+ module: 'attention_optimization'
535
+ });
536
+ }
537
+
538
+ return this.existingComponents.rules;
539
+ }
540
+
541
+ scan() {
542
+ this.techStack = this.scanTechStack();
543
+ this.scanDirectoryStructure();
544
+ this.scanMCP();
545
+ this.scanAgents();
546
+ this.scanRules();
547
+ return this.getReport();
548
+ }
549
+
550
+ getReport() {
551
+ return {
552
+ projectRoot: this.root,
553
+ techStack: this.techStack,
554
+ structure: this.structure,
555
+ fullTopology: this.fullTopology,
556
+ existingComponents: this.existingComponents,
557
+ fileErrors: this.fileErrors,
558
+ stats: {
559
+ mcpCount: this.existingComponents.mcp.length,
560
+ agentCount: this.existingComponents.agents.length,
561
+ ruleCount: this.existingComponents.rules.length,
562
+ l1Rules: this.existingComponents.rules.filter(r => r.level === 'L1').length,
563
+ l2Rules: this.existingComponents.rules.filter(r => r.level === 'L2').length,
564
+ l3Rules: this.existingComponents.rules.filter(r => r.level === 'L3').length,
565
+ customRules: this.existingComponents.rules.filter(r => r.level === 'custom').length,
566
+ topologyNodeCount: this.fullTopology.length
567
+ },
568
+ timestamp: new Date().toISOString()
569
+ };
570
+ }
571
+
572
+ estimateComplexity() {
573
+ let score = 0;
574
+ score += this.techStack.length * 5;
575
+ score += Math.min(this.fullTopology.length, 100);
576
+ score += this.existingComponents.mcp.length * 10;
577
+ score += this.existingComponents.agents.length * 5;
578
+ score += this.existingComponents.rules.length * 3;
579
+
580
+ if (score < 30) return 'simple';
581
+ if (score < 80) return 'medium';
582
+ return 'complex';
583
+ }
584
+ }
585
+
586
+ // ============================================================
587
+ // 第三部分:MCP 服务主逻辑
588
+ // ============================================================
589
+
590
+ // 缓存项目扫描结果
591
+ let projectCache = null;
592
+
593
+ const rl = readline.createInterface({
594
+ input: stdin,
595
+ output: stdout,
596
+ terminal: false
597
+ });
598
+
599
+ rl.on('line', (input) => {
600
+ try {
601
+ const request = JSON.parse(input);
602
+ const { id, method, params } = request;
603
+ let response;
604
+
605
+ if (method === 'initialize') {
606
+ response = {
607
+ jsonrpc: "2.0",
608
+ id: id,
609
+ result: {
610
+ protocolVersion: "2024-11-05",
611
+ capabilities: {
612
+ tools: {}
613
+ },
614
+ serverInfo: {
615
+ name: "rules-enforcer",
616
+ version: "2.0.0",
617
+ description: "规则验证 + 项目扫描整合版"
618
+ }
619
+ }
620
+ };
621
+ } else if (method === 'notifications/initialized') {
622
+ return;
623
+ } else if (method === 'tools/list') {
624
+ response = {
625
+ jsonrpc: "2.0",
626
+ id: id,
627
+ result: {
628
+ tools: [
629
+ // ===== 原有工具 =====
630
+ {
631
+ name: "validate",
632
+ description: "验证代码是否符合L1/L2/L3规则",
633
+ inputSchema: {
634
+ type: "object",
635
+ properties: {
636
+ code: { type: "string", description: "要验证的代码" },
637
+ context: { type: "object", description: "上下文信息" }
638
+ },
639
+ required: ["code"]
640
+ }
641
+ },
642
+ {
643
+ name: "get_rules",
644
+ description: "获取所有规则定义",
645
+ inputSchema: {
646
+ type: "object",
647
+ properties: {}
648
+ }
649
+ },
650
+ {
651
+ name: "enforce",
652
+ description: "强制执行规则验证,不通过则拒绝",
653
+ inputSchema: {
654
+ type: "object",
655
+ properties: {
656
+ code: { type: "string", description: "要验证的代码" },
657
+ context: { type: "object", description: "上下文信息" }
658
+ },
659
+ required: ["code"]
660
+ }
661
+ },
662
+ // ===== 新增工具(来自 project-detector) =====
663
+ {
664
+ name: "scan_project",
665
+ description: "扫描项目结构,识别技术栈、目录拓扑、已有组件",
666
+ inputSchema: {
667
+ type: "object",
668
+ properties: {
669
+ projectPath: { type: "string", description: "项目路径(默认当前工作目录)" },
670
+ maxDepth: { type: "number", description: "最大扫描深度(默认4)" }
671
+ }
672
+ }
673
+ },
674
+ {
675
+ name: "get_tech_stack",
676
+ description: "获取项目技术栈信息",
677
+ inputSchema: {
678
+ type: "object",
679
+ properties: {
680
+ projectPath: { type: "string", description: "项目路径" }
681
+ }
682
+ }
683
+ },
684
+ {
685
+ name: "get_topology",
686
+ description: "获取项目目录拓扑结构",
687
+ inputSchema: {
688
+ type: "object",
689
+ properties: {
690
+ projectPath: { type: "string", description: "项目路径" }
691
+ }
692
+ }
693
+ },
694
+ {
695
+ name: "get_components",
696
+ description: "获取项目中的MCP服务、Agent配置、规则文件",
697
+ inputSchema: {
698
+ type: "object",
699
+ properties: {
700
+ projectPath: { type: "string", description: "项目路径" }
701
+ }
702
+ }
703
+ },
704
+ {
705
+ name: "analyze_project",
706
+ description: "分析项目并生成完整报告",
707
+ inputSchema: {
708
+ type: "object",
709
+ properties: {
710
+ projectPath: { type: "string", description: "项目路径" }
711
+ }
712
+ }
713
+ }
714
+ ]
715
+ }
716
+ };
717
+ } else if (method === 'tools/call') {
718
+ const toolName = params?.name;
719
+ const toolArgs = params?.arguments || {};
720
+
721
+ // ===== 原有工具处理 =====
722
+ if (toolName === 'validate') {
723
+ const issues = validateCode(toolArgs.code, toolArgs.context);
724
+ response = {
725
+ jsonrpc: "2.0",
726
+ id: id,
727
+ result: {
728
+ content: [
729
+ {
730
+ type: "text",
731
+ text: JSON.stringify({
732
+ success: true,
733
+ issues,
734
+ summary: {
735
+ total: issues.length,
736
+ errors: issues.filter(i => i.severity === 'error').length,
737
+ warnings: issues.filter(i => i.severity === 'warning').length,
738
+ info: issues.filter(i => i.severity === 'info').length
739
+ }
740
+ })
741
+ }
742
+ ]
743
+ }
744
+ };
745
+ } else if (toolName === 'get_rules') {
746
+ response = {
747
+ jsonrpc: "2.0",
748
+ id: id,
749
+ result: {
750
+ content: [
751
+ {
752
+ type: "text",
753
+ text: JSON.stringify({
754
+ success: true,
755
+ rules: getRules()
756
+ })
757
+ }
758
+ ]
759
+ }
760
+ };
761
+ } else if (toolName === 'enforce') {
762
+ const issues = validateCode(toolArgs.code, toolArgs.context);
763
+ const errors = issues.filter(i => i.severity === 'error');
764
+ const status = errors.length === 0 ? 'accepted' : 'rejected';
765
+ response = {
766
+ jsonrpc: "2.0",
767
+ id: id,
768
+ result: {
769
+ content: [
770
+ {
771
+ type: "text",
772
+ text: JSON.stringify({
773
+ success: true,
774
+ status,
775
+ issues,
776
+ errors
777
+ })
778
+ }
779
+ ]
780
+ }
781
+ };
782
+ }
783
+ // ===== 新增工具处理(来自 project-detector) =====
784
+ else if (toolName === 'scan_project') {
785
+ try {
786
+ const projectPath = toolArgs.projectPath || process.cwd();
787
+ const maxDepth = toolArgs.maxDepth || 4;
788
+ const detector = new ProjectDetector(projectPath);
789
+ const report = detector.scan();
790
+ projectCache = report;
791
+ response = {
792
+ jsonrpc: "2.0",
793
+ id: id,
794
+ result: {
795
+ content: [
796
+ {
797
+ type: "text",
798
+ text: JSON.stringify({
799
+ success: true,
800
+ report
801
+ })
802
+ }
803
+ ]
804
+ }
805
+ };
806
+ } catch (e) {
807
+ response = {
808
+ jsonrpc: "2.0",
809
+ id: id,
810
+ result: {
811
+ content: [
812
+ {
813
+ type: "text",
814
+ text: JSON.stringify({
815
+ success: false,
816
+ error: e.message
817
+ })
818
+ }
819
+ ]
820
+ }
821
+ };
822
+ }
823
+ } else if (toolName === 'get_tech_stack') {
824
+ try {
825
+ const projectPath = toolArgs.projectPath || process.cwd();
826
+ const detector = new ProjectDetector(projectPath);
827
+ const techStack = detector.scanTechStack();
828
+ response = {
829
+ jsonrpc: "2.0",
830
+ id: id,
831
+ result: {
832
+ content: [
833
+ {
834
+ type: "text",
835
+ text: JSON.stringify({
836
+ success: true,
837
+ techStack,
838
+ count: techStack.length
839
+ })
840
+ }
841
+ ]
842
+ }
843
+ };
844
+ } catch (e) {
845
+ response = {
846
+ jsonrpc: "2.0",
847
+ id: id,
848
+ result: {
849
+ content: [
850
+ {
851
+ type: "text",
852
+ text: JSON.stringify({
853
+ success: false,
854
+ error: e.message
855
+ })
856
+ }
857
+ ]
858
+ }
859
+ };
860
+ }
861
+ } else if (toolName === 'get_topology') {
862
+ try {
863
+ const projectPath = toolArgs.projectPath || process.cwd();
864
+ const detector = new ProjectDetector(projectPath);
865
+ detector.scanDirectoryStructure();
866
+ response = {
867
+ jsonrpc: "2.0",
868
+ id: id,
869
+ result: {
870
+ content: [
871
+ {
872
+ type: "text",
873
+ text: JSON.stringify({
874
+ success: true,
875
+ structure: detector.structure,
876
+ topology: detector.fullTopology,
877
+ nodeCount: detector.fullTopology.length
878
+ })
879
+ }
880
+ ]
881
+ }
882
+ };
883
+ } catch (e) {
884
+ response = {
885
+ jsonrpc: "2.0",
886
+ id: id,
887
+ result: {
888
+ content: [
889
+ {
890
+ type: "text",
891
+ text: JSON.stringify({
892
+ success: false,
893
+ error: e.message
894
+ })
895
+ }
896
+ ]
897
+ }
898
+ };
899
+ }
900
+ } else if (toolName === 'get_components') {
901
+ try {
902
+ const projectPath = toolArgs.projectPath || process.cwd();
903
+ const detector = new ProjectDetector(projectPath);
904
+ detector.scanMCP();
905
+ detector.scanAgents();
906
+ detector.scanRules();
907
+ response = {
908
+ jsonrpc: "2.0",
909
+ id: id,
910
+ result: {
911
+ content: [
912
+ {
913
+ type: "text",
914
+ text: JSON.stringify({
915
+ success: true,
916
+ components: detector.existingComponents,
917
+ stats: {
918
+ mcpCount: detector.existingComponents.mcp.length,
919
+ agentCount: detector.existingComponents.agents.length,
920
+ ruleCount: detector.existingComponents.rules.length
921
+ }
922
+ })
923
+ }
924
+ ]
925
+ }
926
+ };
927
+ } catch (e) {
928
+ response = {
929
+ jsonrpc: "2.0",
930
+ id: id,
931
+ result: {
932
+ content: [
933
+ {
934
+ type: "text",
935
+ text: JSON.stringify({
936
+ success: false,
937
+ error: e.message
938
+ })
939
+ }
940
+ ]
941
+ }
942
+ };
943
+ }
944
+ } else if (toolName === 'analyze_project') {
945
+ try {
946
+ const projectPath = toolArgs.projectPath || process.cwd();
947
+ const detector = new ProjectDetector(projectPath);
948
+ const report = detector.scan();
949
+ const complexity = detector.estimateComplexity();
950
+ response = {
951
+ jsonrpc: "2.0",
952
+ id: id,
953
+ result: {
954
+ content: [
955
+ {
956
+ type: "text",
957
+ text: JSON.stringify({
958
+ success: true,
959
+ analysis: {
960
+ ...report,
961
+ complexity,
962
+ recommendations: generateRecommendations(report)
963
+ }
964
+ })
965
+ }
966
+ ]
967
+ }
968
+ };
969
+ } catch (e) {
970
+ response = {
971
+ jsonrpc: "2.0",
972
+ id: id,
973
+ result: {
974
+ content: [
975
+ {
976
+ type: "text",
977
+ text: JSON.stringify({
978
+ success: false,
979
+ error: e.message
980
+ })
981
+ }
982
+ ]
983
+ }
984
+ };
985
+ }
986
+ } else {
987
+ response = {
988
+ jsonrpc: "2.0",
989
+ id: id,
990
+ error: {
991
+ code: -32601,
992
+ message: 'Tool not found'
993
+ }
994
+ };
995
+ }
996
+ } else {
997
+ response = {
998
+ jsonrpc: "2.0",
999
+ id: id,
1000
+ error: {
1001
+ code: -32601,
1002
+ message: 'Method not found'
1003
+ }
1004
+ };
1005
+ }
1006
+
1007
+ stdout.write(JSON.stringify(response) + '\n');
1008
+ } catch (error) {
1009
+ stdout.write(JSON.stringify({
1010
+ jsonrpc: "2.0",
1011
+ id: null,
1012
+ error: {
1013
+ code: -32603,
1014
+ message: 'Internal error: ' + error.message
1015
+ }
1016
+ }) + '\n');
1017
+ }
1018
+ });
1019
+
1020
+ /**
1021
+ * 生成项目优化建议
1022
+ */
1023
+ function generateRecommendations(report) {
1024
+ const recommendations = [];
1025
+
1026
+ // 技术栈建议
1027
+ if (report.techStack.includes('Node.js') && !report.techStack.includes('TypeScript')) {
1028
+ recommendations.push({
1029
+ type: 'tech_stack',
1030
+ message: '建议引入TypeScript提升代码质量',
1031
+ priority: 'medium'
1032
+ });
1033
+ }
1034
+
1035
+ // MCP服务建议
1036
+ if (report.stats.mcpCount === 0) {
1037
+ recommendations.push({
1038
+ type: 'mcp',
1039
+ message: '项目未配置MCP服务,建议添加rules-enforcer',
1040
+ priority: 'high'
1041
+ });
1042
+ }
1043
+
1044
+ // 规则建议
1045
+ if (report.stats.l1Rules === 0) {
1046
+ recommendations.push({
1047
+ type: 'rules',
1048
+ message: '缺少L1用户规则,建议添加基础代码规范',
1049
+ priority: 'medium'
1050
+ });
1051
+ }
1052
+
1053
+ if (report.stats.l2Rules === 0) {
1054
+ recommendations.push({
1055
+ type: 'rules',
1056
+ message: '缺少L2项目规则,建议添加项目特定规范',
1057
+ priority: 'medium'
1058
+ });
1059
+ }
1060
+
1061
+ return recommendations;
1062
+ }
1063
+
1064
+ // 启动日志
1065
+ console.error('[rules-enforcer v2.0.0] MCP服务已启动');
1066
+ console.error('[rules-enforcer] 整合功能: 规则验证 + 项目扫描');