@su-record/vibe 0.1.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.
- package/LICENSE +21 -0
- package/README.md +448 -0
- package/agents/backend-python-expert.md +453 -0
- package/agents/database-postgres-expert.md +538 -0
- package/agents/frontend-flutter-expert.md +487 -0
- package/agents/frontend-react-expert.md +424 -0
- package/agents/quality-reviewer.md +542 -0
- package/agents/specification-agent.md +505 -0
- package/bin/sutory +332 -0
- package/bin/vibe +338 -0
- package/mcp/dist/__tests__/complexity.test.js +126 -0
- package/mcp/dist/__tests__/memory.test.js +120 -0
- package/mcp/dist/__tests__/python-dart-complexity.test.js +146 -0
- package/mcp/dist/index.js +230 -0
- package/mcp/dist/lib/ContextCompressor.js +305 -0
- package/mcp/dist/lib/MemoryManager.js +334 -0
- package/mcp/dist/lib/ProjectCache.js +126 -0
- package/mcp/dist/lib/PythonParser.js +241 -0
- package/mcp/dist/tools/browser/browserPool.js +76 -0
- package/mcp/dist/tools/browser/browserUtils.js +135 -0
- package/mcp/dist/tools/browser/inspectNetworkRequests.js +140 -0
- package/mcp/dist/tools/browser/monitorConsoleLogs.js +97 -0
- package/mcp/dist/tools/convention/analyzeComplexity.js +248 -0
- package/mcp/dist/tools/convention/applyQualityRules.js +102 -0
- package/mcp/dist/tools/convention/checkCouplingCohesion.js +233 -0
- package/mcp/dist/tools/convention/complexityMetrics.js +133 -0
- package/mcp/dist/tools/convention/dartComplexity.js +117 -0
- package/mcp/dist/tools/convention/getCodingGuide.js +64 -0
- package/mcp/dist/tools/convention/languageDetector.js +50 -0
- package/mcp/dist/tools/convention/pythonComplexity.js +109 -0
- package/mcp/dist/tools/convention/suggestImprovements.js +257 -0
- package/mcp/dist/tools/convention/validateCodeQuality.js +177 -0
- package/mcp/dist/tools/memory/autoSaveContext.js +79 -0
- package/mcp/dist/tools/memory/database.js +123 -0
- package/mcp/dist/tools/memory/deleteMemory.js +39 -0
- package/mcp/dist/tools/memory/listMemories.js +38 -0
- package/mcp/dist/tools/memory/memoryConfig.js +27 -0
- package/mcp/dist/tools/memory/memorySQLite.js +138 -0
- package/mcp/dist/tools/memory/memoryUtils.js +34 -0
- package/mcp/dist/tools/memory/migrate.js +113 -0
- package/mcp/dist/tools/memory/prioritizeMemory.js +109 -0
- package/mcp/dist/tools/memory/recallMemory.js +40 -0
- package/mcp/dist/tools/memory/restoreSessionContext.js +69 -0
- package/mcp/dist/tools/memory/saveMemory.js +34 -0
- package/mcp/dist/tools/memory/searchMemories.js +37 -0
- package/mcp/dist/tools/memory/startSession.js +100 -0
- package/mcp/dist/tools/memory/updateMemory.js +46 -0
- package/mcp/dist/tools/planning/analyzeRequirements.js +166 -0
- package/mcp/dist/tools/planning/createUserStories.js +119 -0
- package/mcp/dist/tools/planning/featureRoadmap.js +202 -0
- package/mcp/dist/tools/planning/generatePrd.js +156 -0
- package/mcp/dist/tools/prompt/analyzePrompt.js +145 -0
- package/mcp/dist/tools/prompt/enhancePrompt.js +105 -0
- package/mcp/dist/tools/semantic/findReferences.js +195 -0
- package/mcp/dist/tools/semantic/findSymbol.js +200 -0
- package/mcp/dist/tools/thinking/analyzeProblem.js +50 -0
- package/mcp/dist/tools/thinking/breakDownProblem.js +140 -0
- package/mcp/dist/tools/thinking/createThinkingChain.js +39 -0
- package/mcp/dist/tools/thinking/formatAsPlan.js +73 -0
- package/mcp/dist/tools/thinking/stepByStepAnalysis.js +58 -0
- package/mcp/dist/tools/thinking/thinkAloudProcess.js +75 -0
- package/mcp/dist/tools/time/getCurrentTime.js +61 -0
- package/mcp/dist/tools/ui/previewUiAscii.js +232 -0
- package/mcp/dist/types/tool.js +2 -0
- package/mcp/package.json +53 -0
- package/package.json +49 -0
- package/scripts/install-mcp.js +48 -0
- package/scripts/install.sh +70 -0
- package/skills/core/communication-guide.md +104 -0
- package/skills/core/development-philosophy.md +53 -0
- package/skills/core/quick-start.md +121 -0
- package/skills/languages/dart-flutter.md +509 -0
- package/skills/languages/python-fastapi.md +386 -0
- package/skills/languages/typescript-nextjs.md +441 -0
- package/skills/languages/typescript-react-native.md +446 -0
- package/skills/languages/typescript-react.md +525 -0
- package/skills/quality/checklist.md +276 -0
- package/skills/quality/testing-strategy.md +437 -0
- package/skills/standards/anti-patterns.md +369 -0
- package/skills/standards/code-structure.md +291 -0
- package/skills/standards/complexity-metrics.md +312 -0
- package/skills/standards/naming-conventions.md +198 -0
- package/skills/tools/mcp-hi-ai-guide.md +665 -0
- package/skills/tools/mcp-workflow.md +51 -0
- package/templates/constitution-template.md +193 -0
- package/templates/plan-template.md +237 -0
- package/templates/spec-template.md +142 -0
- package/templates/tasks-template.md +132 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Browser development tool - completely independent
|
|
2
|
+
import puppeteer from 'puppeteer-core';
|
|
3
|
+
import { getBrowserLaunchOptions } from './browserUtils.js';
|
|
4
|
+
export const inspectNetworkRequestsDefinition = {
|
|
5
|
+
name: 'inspect_network_requests',
|
|
6
|
+
description: '네트워크|API 호출|요청 확인|network|API calls|check requests|network traffic - Inspect network requests',
|
|
7
|
+
inputSchema: {
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: {
|
|
10
|
+
url: { type: 'string', description: 'URL to inspect' },
|
|
11
|
+
filterType: { type: 'string', description: 'Request type filter', enum: ['all', 'xhr', 'fetch', 'websocket', 'failed'] },
|
|
12
|
+
includeHeaders: { type: 'boolean', description: 'Include request/response headers' }
|
|
13
|
+
},
|
|
14
|
+
required: ['url']
|
|
15
|
+
},
|
|
16
|
+
annotations: {
|
|
17
|
+
title: 'Inspect Network Requests',
|
|
18
|
+
audience: ['user', 'assistant']
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
export async function inspectNetworkRequests(args) {
|
|
22
|
+
const { url: inspectUrl, filterType = 'all', includeHeaders = false } = args;
|
|
23
|
+
try {
|
|
24
|
+
// Get browser launch options with proper executable path
|
|
25
|
+
const launchOptions = getBrowserLaunchOptions();
|
|
26
|
+
const browser = await puppeteer.launch(launchOptions);
|
|
27
|
+
const page = await browser.newPage();
|
|
28
|
+
const networkRequests = [];
|
|
29
|
+
let requestId = 0;
|
|
30
|
+
const requestTimings = new Map();
|
|
31
|
+
// Capture network requests
|
|
32
|
+
page.on('request', request => {
|
|
33
|
+
const startTime = Date.now();
|
|
34
|
+
const id = `req_${String(requestId++).padStart(3, '0')}`;
|
|
35
|
+
const requestUrl = request.url();
|
|
36
|
+
requestTimings.set(requestUrl, startTime);
|
|
37
|
+
networkRequests.push({
|
|
38
|
+
id,
|
|
39
|
+
url: requestUrl,
|
|
40
|
+
method: request.method(),
|
|
41
|
+
type: request.resourceType(),
|
|
42
|
+
responseTime: 0,
|
|
43
|
+
size: 0,
|
|
44
|
+
timestamp: new Date().toISOString(),
|
|
45
|
+
headers: includeHeaders ? {
|
|
46
|
+
request: request.headers()
|
|
47
|
+
} : undefined
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
page.on('response', async (response) => {
|
|
51
|
+
const requestUrl = response.url();
|
|
52
|
+
const request = networkRequests.find(req => req.url === requestUrl);
|
|
53
|
+
const startTime = requestTimings.get(requestUrl);
|
|
54
|
+
if (request) {
|
|
55
|
+
request.status = response.status();
|
|
56
|
+
request.statusText = response.statusText();
|
|
57
|
+
request.responseTime = startTime ? Date.now() - startTime : 0;
|
|
58
|
+
request.failed = !response.ok();
|
|
59
|
+
if (includeHeaders && request.headers) {
|
|
60
|
+
request.headers.response = response.headers();
|
|
61
|
+
}
|
|
62
|
+
// Estimate response size
|
|
63
|
+
try {
|
|
64
|
+
const buffer = await response.buffer();
|
|
65
|
+
request.size = buffer.length;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
request.size = 0;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
page.on('requestfailed', request => {
|
|
73
|
+
const requestUrl = request.url();
|
|
74
|
+
const failedRequest = networkRequests.find(req => req.url === requestUrl);
|
|
75
|
+
if (failedRequest) {
|
|
76
|
+
failedRequest.failed = true;
|
|
77
|
+
failedRequest.status = 0;
|
|
78
|
+
failedRequest.statusText = request.failure()?.errorText || 'Failed';
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
// Navigate to URL and wait for network to be idle
|
|
82
|
+
await page.goto(inspectUrl, { waitUntil: 'networkidle0', timeout: 30000 });
|
|
83
|
+
// Wait a bit for any remaining requests
|
|
84
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
85
|
+
await browser.close();
|
|
86
|
+
const filteredRequests = networkRequests.filter(req => {
|
|
87
|
+
switch (filterType) {
|
|
88
|
+
case 'xhr':
|
|
89
|
+
return req.type === 'xhr';
|
|
90
|
+
case 'fetch':
|
|
91
|
+
return req.type === 'fetch';
|
|
92
|
+
case 'websocket':
|
|
93
|
+
return req.type === 'websocket';
|
|
94
|
+
case 'failed':
|
|
95
|
+
return req.failed || (req.status !== undefined && req.status >= 400);
|
|
96
|
+
default:
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
const networkInspectionResult = {
|
|
101
|
+
action: 'inspect_network_requests',
|
|
102
|
+
url: inspectUrl,
|
|
103
|
+
filterType,
|
|
104
|
+
includeHeaders,
|
|
105
|
+
requests: filteredRequests,
|
|
106
|
+
summary: {
|
|
107
|
+
totalRequests: filteredRequests.length,
|
|
108
|
+
successful: filteredRequests.filter(r => r.status !== undefined && r.status >= 200 && r.status < 300).length,
|
|
109
|
+
failed: filteredRequests.filter(r => r.failed || (r.status !== undefined && r.status >= 400)).length,
|
|
110
|
+
averageResponseTime: filteredRequests.reduce((sum, r) => sum + r.responseTime, 0) / filteredRequests.length,
|
|
111
|
+
totalDataTransferred: filteredRequests.reduce((sum, r) => sum + r.size, 0),
|
|
112
|
+
requestTypes: {
|
|
113
|
+
xhr: filteredRequests.filter(r => r.type === 'xhr').length,
|
|
114
|
+
fetch: filteredRequests.filter(r => r.type === 'fetch').length,
|
|
115
|
+
websocket: filteredRequests.filter(r => r.type === 'websocket').length
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
status: 'success'
|
|
119
|
+
};
|
|
120
|
+
// Compact summary format
|
|
121
|
+
const failed = filteredRequests.filter(r => r.failed || (r.status !== undefined && r.status >= 400));
|
|
122
|
+
const errorSummary = failed.length > 0
|
|
123
|
+
? `\nErrors: ${failed.slice(0, 3).map(r => `${r.method} ${new URL(r.url).pathname} (${r.status})`).join(', ')}${failed.length > 3 ? ` +${failed.length - 3}` : ''}`
|
|
124
|
+
: '';
|
|
125
|
+
return {
|
|
126
|
+
content: [{
|
|
127
|
+
type: 'text',
|
|
128
|
+
text: `${networkInspectionResult.summary.totalRequests} reqs | ${networkInspectionResult.summary.successful} OK, ${networkInspectionResult.summary.failed} fail | Avg: ${networkInspectionResult.summary.averageResponseTime.toFixed(0)}ms | ${(networkInspectionResult.summary.totalDataTransferred / 1024).toFixed(1)}KB${errorSummary}`
|
|
129
|
+
}]
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
134
|
+
const helpMessage = errorMessage.includes('Chrome') ?
|
|
135
|
+
'\n\nTroubleshooting:\n1. Install Chrome: https://www.google.com/chrome/\n2. Or set CHROME_PATH environment variable\n3. Or install puppeteer instead of puppeteer-core' : '';
|
|
136
|
+
return {
|
|
137
|
+
content: [{ type: 'text', text: `Error inspecting network requests: ${errorMessage}${helpMessage}` }]
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Browser development tool - completely independent
|
|
2
|
+
import puppeteer from 'puppeteer-core';
|
|
3
|
+
import { getBrowserLaunchOptions } from './browserUtils.js';
|
|
4
|
+
export const monitorConsoleLogsDefinition = {
|
|
5
|
+
name: 'monitor_console_logs',
|
|
6
|
+
description: '콘솔 로그|에러 확인|로그 봐줘|console|check logs|debug output|console errors - Monitor browser console',
|
|
7
|
+
inputSchema: {
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: {
|
|
10
|
+
url: { type: 'string', description: 'URL to monitor' },
|
|
11
|
+
logLevel: { type: 'string', description: 'Log level to capture', enum: ['all', 'error', 'warn', 'info', 'debug'] },
|
|
12
|
+
duration: { type: 'number', description: 'Monitoring duration in seconds' }
|
|
13
|
+
},
|
|
14
|
+
required: ['url']
|
|
15
|
+
},
|
|
16
|
+
annotations: {
|
|
17
|
+
title: 'Monitor Console Logs',
|
|
18
|
+
audience: ['user', 'assistant']
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
export async function monitorConsoleLogs(args) {
|
|
22
|
+
const { url: monitorUrl, logLevel = 'all', duration = 30 } = args;
|
|
23
|
+
try {
|
|
24
|
+
// Get browser launch options with proper executable path
|
|
25
|
+
const launchOptions = getBrowserLaunchOptions();
|
|
26
|
+
const browser = await puppeteer.launch(launchOptions);
|
|
27
|
+
const page = await browser.newPage();
|
|
28
|
+
const logs = [];
|
|
29
|
+
// Capture console events
|
|
30
|
+
page.on('console', msg => {
|
|
31
|
+
const msgLevel = msg.type();
|
|
32
|
+
if (logLevel === 'all' || msgLevel === logLevel) {
|
|
33
|
+
logs.push({
|
|
34
|
+
timestamp: new Date().toISOString(),
|
|
35
|
+
level: msgLevel,
|
|
36
|
+
message: msg.text(),
|
|
37
|
+
source: msg.location()?.url || 'unknown'
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
// Capture page errors
|
|
42
|
+
page.on('pageerror', error => {
|
|
43
|
+
if (logLevel === 'all' || logLevel === 'error') {
|
|
44
|
+
logs.push({
|
|
45
|
+
timestamp: new Date().toISOString(),
|
|
46
|
+
level: 'error',
|
|
47
|
+
message: error.message,
|
|
48
|
+
source: error.stack?.split('\n')[0] || 'unknown'
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
// Navigate to URL and wait for specified duration
|
|
53
|
+
await page.goto(monitorUrl, { waitUntil: 'networkidle0', timeout: 30000 });
|
|
54
|
+
await new Promise(resolve => setTimeout(resolve, duration * 1000));
|
|
55
|
+
await browser.close();
|
|
56
|
+
const consoleMonitorResult = {
|
|
57
|
+
action: 'monitor_console_logs',
|
|
58
|
+
url: monitorUrl,
|
|
59
|
+
logLevel,
|
|
60
|
+
duration,
|
|
61
|
+
capturedLogs: logs,
|
|
62
|
+
summary: {
|
|
63
|
+
totalLogs: logs.length,
|
|
64
|
+
errors: logs.filter(l => l.level === 'error').length,
|
|
65
|
+
warnings: logs.filter(l => l.level === 'warn').length,
|
|
66
|
+
infos: logs.filter(l => l.level === 'info').length,
|
|
67
|
+
debugs: logs.filter(l => l.level === 'debug').length,
|
|
68
|
+
logs: logs.filter(l => l.level === 'log').length
|
|
69
|
+
},
|
|
70
|
+
monitoringStatus: 'completed',
|
|
71
|
+
status: 'success'
|
|
72
|
+
};
|
|
73
|
+
// Compact summary with errors only
|
|
74
|
+
const errors = logs.filter(l => l.level === 'error');
|
|
75
|
+
const warnings = logs.filter(l => l.level === 'warn');
|
|
76
|
+
const errorSummary = errors.length > 0
|
|
77
|
+
? `\nErrors: ${errors.slice(0, 3).map(l => l.message.substring(0, 50)).join(', ')}${errors.length > 3 ? ` +${errors.length - 3}` : ''}`
|
|
78
|
+
: '';
|
|
79
|
+
const warnSummary = warnings.length > 0 && errors.length === 0
|
|
80
|
+
? `\nWarnings: ${warnings.slice(0, 3).map(l => l.message.substring(0, 50)).join(', ')}${warnings.length > 3 ? ` +${warnings.length - 3}` : ''}`
|
|
81
|
+
: '';
|
|
82
|
+
return {
|
|
83
|
+
content: [{
|
|
84
|
+
type: 'text',
|
|
85
|
+
text: `${logs.length} logs | ${consoleMonitorResult.summary.errors}E ${consoleMonitorResult.summary.warnings}W ${consoleMonitorResult.summary.infos}I | ${duration}s${errorSummary}${warnSummary}`
|
|
86
|
+
}]
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
91
|
+
const helpMessage = errorMessage.includes('Chrome') ?
|
|
92
|
+
'\n\nTroubleshooting:\n1. Install Chrome: https://www.google.com/chrome/\n2. Or set CHROME_PATH environment variable\n3. Or install puppeteer instead of puppeteer-core' : '';
|
|
93
|
+
return {
|
|
94
|
+
content: [{ type: 'text', text: `Error monitoring console logs: ${errorMessage}${helpMessage}` }]
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// Convention management tool - completely independent
|
|
2
|
+
import { Project, ScriptKind } from "ts-morph";
|
|
3
|
+
import { PythonParser } from '../../lib/PythonParser.js';
|
|
4
|
+
// Reusable in-memory project to avoid re-parsing standard lib every call
|
|
5
|
+
const AST_PROJECT = new Project({
|
|
6
|
+
useInMemoryFileSystem: true,
|
|
7
|
+
compilerOptions: { allowJs: true, skipLibCheck: true }
|
|
8
|
+
});
|
|
9
|
+
// Enhanced Software Engineering Metrics
|
|
10
|
+
const CODE_QUALITY_METRICS = {
|
|
11
|
+
COMPLEXITY: {
|
|
12
|
+
maxCyclomaticComplexity: 10,
|
|
13
|
+
maxCognitiveComplexity: 15,
|
|
14
|
+
maxFunctionLines: 20,
|
|
15
|
+
maxNestingDepth: 3,
|
|
16
|
+
maxParameters: 5
|
|
17
|
+
},
|
|
18
|
+
COUPLING: {
|
|
19
|
+
maxDependencies: 7,
|
|
20
|
+
maxFanOut: 5,
|
|
21
|
+
preventCircularDeps: true
|
|
22
|
+
},
|
|
23
|
+
COHESION: {
|
|
24
|
+
singleResponsibility: true,
|
|
25
|
+
relatedFunctionsOnly: true
|
|
26
|
+
},
|
|
27
|
+
MAINTAINABILITY: {
|
|
28
|
+
noMagicNumbers: true,
|
|
29
|
+
consistentNaming: true,
|
|
30
|
+
properErrorHandling: true,
|
|
31
|
+
typesSafety: true
|
|
32
|
+
},
|
|
33
|
+
PERFORMANCE: {
|
|
34
|
+
memoizeExpensiveCalc: true,
|
|
35
|
+
lazyLoading: true,
|
|
36
|
+
batchOperations: true
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
export const analyzeComplexityDefinition = {
|
|
40
|
+
name: 'analyze_complexity',
|
|
41
|
+
description: '복잡도|복잡한지|complexity|how complex|난이도 - Analyze code complexity',
|
|
42
|
+
inputSchema: {
|
|
43
|
+
type: 'object',
|
|
44
|
+
properties: {
|
|
45
|
+
code: { type: 'string', description: 'Code to analyze' },
|
|
46
|
+
metrics: { type: 'string', description: 'Metrics to calculate', enum: ['cyclomatic', 'cognitive', 'halstead', 'all'] }
|
|
47
|
+
},
|
|
48
|
+
required: ['code']
|
|
49
|
+
},
|
|
50
|
+
annotations: {
|
|
51
|
+
title: 'Analyze Complexity',
|
|
52
|
+
audience: ['user', 'assistant']
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Calculate cognitive complexity (how hard code is to understand)
|
|
57
|
+
*/
|
|
58
|
+
function calculateCognitiveComplexity(code) {
|
|
59
|
+
const CONTROL_STRUCTURES = ['if', 'for', 'while'];
|
|
60
|
+
let cognitiveScore = 0;
|
|
61
|
+
const lines = code.split('\n');
|
|
62
|
+
let nestingLevel = 0;
|
|
63
|
+
for (const line of lines) {
|
|
64
|
+
const trimmed = line.trim();
|
|
65
|
+
// Increment for control structures
|
|
66
|
+
if (CONTROL_STRUCTURES.some(keyword => trimmed.includes(keyword))) {
|
|
67
|
+
cognitiveScore += 1 + nestingLevel;
|
|
68
|
+
}
|
|
69
|
+
// Increment for catch/switch
|
|
70
|
+
if (trimmed.includes('catch') || trimmed.includes('switch')) {
|
|
71
|
+
cognitiveScore += 1 + nestingLevel;
|
|
72
|
+
}
|
|
73
|
+
// Update nesting level
|
|
74
|
+
const openBraces = (line.match(/\{/g) || []).length;
|
|
75
|
+
const closeBraces = (line.match(/\}/g) || []).length;
|
|
76
|
+
nestingLevel = Math.max(0, nestingLevel + openBraces - closeBraces);
|
|
77
|
+
}
|
|
78
|
+
const threshold = CODE_QUALITY_METRICS.COMPLEXITY.maxCognitiveComplexity;
|
|
79
|
+
return {
|
|
80
|
+
value: cognitiveScore,
|
|
81
|
+
threshold,
|
|
82
|
+
status: cognitiveScore <= threshold ? 'pass' : 'fail',
|
|
83
|
+
description: 'How difficult the code is to understand'
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Calculate AST-based cyclomatic complexity
|
|
88
|
+
*/
|
|
89
|
+
function calculateAstComplexity(code) {
|
|
90
|
+
const CONTROL_FLOW_NODES = [
|
|
91
|
+
'IfStatement', 'ForStatement', 'ForOfStatement', 'ForInStatement',
|
|
92
|
+
'WhileStatement', 'CaseClause', 'ConditionalExpression',
|
|
93
|
+
'DoStatement', 'CatchClause', 'BinaryExpression'
|
|
94
|
+
];
|
|
95
|
+
let astCyclomatic = 1;
|
|
96
|
+
try {
|
|
97
|
+
const sourceFile = AST_PROJECT.createSourceFile('temp.ts', code, {
|
|
98
|
+
overwrite: true,
|
|
99
|
+
scriptKind: ScriptKind.TS
|
|
100
|
+
});
|
|
101
|
+
sourceFile.forEachDescendant((node) => {
|
|
102
|
+
if (CONTROL_FLOW_NODES.includes(node.getKindName())) {
|
|
103
|
+
astCyclomatic++;
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
const threshold = CODE_QUALITY_METRICS.COMPLEXITY.maxCyclomaticComplexity;
|
|
107
|
+
return {
|
|
108
|
+
value: astCyclomatic,
|
|
109
|
+
threshold,
|
|
110
|
+
status: astCyclomatic <= threshold ? 'pass' : 'fail',
|
|
111
|
+
description: 'AST 기반 분기/조건문 수를 통한 cyclomatic complexity'
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
return {
|
|
116
|
+
value: null,
|
|
117
|
+
status: 'error',
|
|
118
|
+
description: 'AST 분석 실패: ' + (e instanceof Error ? e.message : String(e))
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Analyze Python code complexity
|
|
124
|
+
*/
|
|
125
|
+
async function analyzePythonComplexity(code) {
|
|
126
|
+
try {
|
|
127
|
+
const pythonComplexity = await PythonParser.analyzeComplexity(code);
|
|
128
|
+
const totalComplexity = pythonComplexity.cyclomaticComplexity;
|
|
129
|
+
const issues = [];
|
|
130
|
+
const MAX_COMPLEXITY = 10;
|
|
131
|
+
if (totalComplexity > MAX_COMPLEXITY) {
|
|
132
|
+
issues.push('High complexity');
|
|
133
|
+
}
|
|
134
|
+
pythonComplexity.functions.forEach(f => {
|
|
135
|
+
if (f.complexity > MAX_COMPLEXITY) {
|
|
136
|
+
issues.push(`Function ${f.name}: complexity ${f.complexity}`);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
const issuesText = issues.length ? `\nIssues: ${issues.join(', ')}` : '';
|
|
140
|
+
return {
|
|
141
|
+
content: [{
|
|
142
|
+
type: 'text',
|
|
143
|
+
text: `Python Complexity: ${totalComplexity}\nFunctions: ${pythonComplexity.functions.length}\nClasses: ${pythonComplexity.classes.length}${issuesText}`
|
|
144
|
+
}]
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
return {
|
|
149
|
+
content: [{
|
|
150
|
+
type: 'text',
|
|
151
|
+
text: `Python analysis error: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
152
|
+
}]
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
export async function analyzeComplexity(args) {
|
|
157
|
+
const { code: complexityCode, metrics: complexityMetrics = 'all' } = args;
|
|
158
|
+
// Check if this is Python code
|
|
159
|
+
if (PythonParser.isPythonCode(complexityCode)) {
|
|
160
|
+
return analyzePythonComplexity(complexityCode);
|
|
161
|
+
}
|
|
162
|
+
const complexityAnalysis = {
|
|
163
|
+
action: 'analyze_complexity',
|
|
164
|
+
metrics: complexityMetrics,
|
|
165
|
+
results: {},
|
|
166
|
+
overallScore: 0,
|
|
167
|
+
issues: [],
|
|
168
|
+
recommendations: [],
|
|
169
|
+
status: 'pending'
|
|
170
|
+
};
|
|
171
|
+
// AST 기반 cyclomatic complexity 분석
|
|
172
|
+
complexityAnalysis.results.astCyclomaticComplexity = calculateAstComplexity(complexityCode);
|
|
173
|
+
if (complexityMetrics === 'cyclomatic' || complexityMetrics === 'all') {
|
|
174
|
+
const cyclomaticComplexityScore = (complexityCode.match(/\bif\b|\bfor\b|\bwhile\b|\bcase\b|\b&&\b|\b\|\|\b/g) || []).length + 1;
|
|
175
|
+
complexityAnalysis.results.cyclomaticComplexity = {
|
|
176
|
+
value: cyclomaticComplexityScore,
|
|
177
|
+
threshold: CODE_QUALITY_METRICS.COMPLEXITY.maxCyclomaticComplexity,
|
|
178
|
+
status: cyclomaticComplexityScore <= CODE_QUALITY_METRICS.COMPLEXITY.maxCyclomaticComplexity ? 'pass' : 'fail',
|
|
179
|
+
description: 'Number of linearly independent paths through the code'
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
if (complexityMetrics === 'cognitive' || complexityMetrics === 'all') {
|
|
183
|
+
complexityAnalysis.results.cognitiveComplexity = calculateCognitiveComplexity(complexityCode);
|
|
184
|
+
}
|
|
185
|
+
if (complexityMetrics === 'halstead' || complexityMetrics === 'all') {
|
|
186
|
+
// Halstead metrics calculation (simplified version)
|
|
187
|
+
const operators = (complexityCode.match(/[+\-*/=<>!&|%^~?:]/g) || []).length;
|
|
188
|
+
const operands = (complexityCode.match(/\b[a-zA-Z_]\w*\b/g) || []).length;
|
|
189
|
+
const uniqueOperators = new Set(complexityCode.match(/[+\-*/=<>!&|%^~?:]/g) || []).size;
|
|
190
|
+
const uniqueOperands = new Set(complexityCode.match(/\b[a-zA-Z_]\w*\b/g) || []).size;
|
|
191
|
+
const vocabulary = uniqueOperators + uniqueOperands;
|
|
192
|
+
const length = operators + operands;
|
|
193
|
+
const calculatedLength = vocabulary > 0 ? uniqueOperators * Math.log2(uniqueOperators) + uniqueOperands * Math.log2(uniqueOperands) : 0;
|
|
194
|
+
const volume = length * Math.log2(vocabulary);
|
|
195
|
+
const difficulty = vocabulary > 0 ? (uniqueOperators / 2) * (operands / uniqueOperands) : 0;
|
|
196
|
+
const effort = difficulty * volume;
|
|
197
|
+
complexityAnalysis.results.halsteadMetrics = {
|
|
198
|
+
vocabulary: vocabulary,
|
|
199
|
+
length: length,
|
|
200
|
+
calculatedLength: Math.round(calculatedLength),
|
|
201
|
+
volume: Math.round(volume),
|
|
202
|
+
difficulty: Math.round(difficulty * 100) / 100,
|
|
203
|
+
effort: Math.round(effort),
|
|
204
|
+
timeToProgram: Math.round(effort / 18), // Halstead's formula: effort / 18 seconds
|
|
205
|
+
bugsDelivered: Math.round(volume / 3000 * 100) / 100, // Halstead's formula: volume / 3000
|
|
206
|
+
description: 'Software science metrics measuring program complexity'
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
// Additional complexity metrics
|
|
210
|
+
if (complexityMetrics === 'all') {
|
|
211
|
+
const lines = complexityCode.split('\n');
|
|
212
|
+
const nonEmptyLines = lines.filter(line => line.trim().length > 0).length;
|
|
213
|
+
const comments = (complexityCode.match(/\/\*[\s\S]*?\*\/|\/\/.*$/gm) || []).length;
|
|
214
|
+
const functions = (complexityCode.match(/function\s+\w+|\w+\s*=\s*\(/g) || []).length;
|
|
215
|
+
const classes = (complexityCode.match(/class\s+\w+/g) || []).length;
|
|
216
|
+
complexityAnalysis.results.additionalMetrics = {
|
|
217
|
+
linesOfCode: nonEmptyLines,
|
|
218
|
+
comments: comments,
|
|
219
|
+
commentRatio: nonEmptyLines > 0 ? Math.round((comments / nonEmptyLines) * 100) / 100 : 0,
|
|
220
|
+
functions: functions,
|
|
221
|
+
classes: classes,
|
|
222
|
+
averageFunctionLength: functions > 0 ? Math.round(nonEmptyLines / functions) : 0
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
// Overall assessment
|
|
226
|
+
const issues = [];
|
|
227
|
+
let overallScore = 100;
|
|
228
|
+
if (complexityAnalysis.results.cyclomaticComplexity && complexityAnalysis.results.cyclomaticComplexity.status === 'fail') {
|
|
229
|
+
issues.push('High cyclomatic complexity detected');
|
|
230
|
+
overallScore -= 20;
|
|
231
|
+
}
|
|
232
|
+
if (complexityAnalysis.results.cognitiveComplexity && complexityAnalysis.results.cognitiveComplexity.status === 'fail') {
|
|
233
|
+
issues.push('High cognitive complexity detected');
|
|
234
|
+
overallScore -= 25;
|
|
235
|
+
}
|
|
236
|
+
if (complexityAnalysis.results.halsteadMetrics && complexityAnalysis.results.halsteadMetrics.difficulty > 10) {
|
|
237
|
+
issues.push('High Halstead difficulty detected');
|
|
238
|
+
overallScore -= 15;
|
|
239
|
+
}
|
|
240
|
+
complexityAnalysis.overallScore = Math.max(0, overallScore);
|
|
241
|
+
complexityAnalysis.issues = issues;
|
|
242
|
+
return {
|
|
243
|
+
content: [{
|
|
244
|
+
type: 'text',
|
|
245
|
+
text: `Complexity: ${complexityAnalysis.results.astCyclomaticComplexity?.value ?? 'N/A'}\nScore: ${complexityAnalysis.overallScore}${issues.length ? '\nIssues: ' + issues.join(', ') : ''}`
|
|
246
|
+
}]
|
|
247
|
+
};
|
|
248
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Convention management tool - completely independent
|
|
2
|
+
// Code Quality Standards
|
|
3
|
+
const QUALITY_RULES = {
|
|
4
|
+
NAMING: {
|
|
5
|
+
variables: 'nouns (userList, userData)',
|
|
6
|
+
functions: 'verb+noun (fetchData, updateUser)',
|
|
7
|
+
events: 'handle prefix (handleClick, handleSubmit)',
|
|
8
|
+
booleans: 'is/has/can prefix (isLoading, hasError, canEdit)',
|
|
9
|
+
constants: 'UPPER_SNAKE_CASE (MAX_RETRY_COUNT, API_TIMEOUT)',
|
|
10
|
+
components: 'PascalCase (UserProfile, HeaderSection)',
|
|
11
|
+
hooks: 'use prefix (useUserData, useAuth)'
|
|
12
|
+
},
|
|
13
|
+
STRUCTURE: {
|
|
14
|
+
componentOrder: ['State & Refs', 'Custom Hooks', 'Event Handlers', 'Effects', 'Early returns', 'Main return JSX'],
|
|
15
|
+
functionMaxLines: 20,
|
|
16
|
+
componentMaxLines: 50,
|
|
17
|
+
maxNestingDepth: 3
|
|
18
|
+
},
|
|
19
|
+
ANTIPATTERNS: {
|
|
20
|
+
typescript: ['any type usage', '@ts-ignore usage', 'as any casting'],
|
|
21
|
+
react: ['dangerouslySetInnerHTML', 'props drilling (3+ levels)'],
|
|
22
|
+
javascript: ['var usage', '== instead of ===', 'eval() usage'],
|
|
23
|
+
css: ['!important abuse', 'inline style abuse']
|
|
24
|
+
},
|
|
25
|
+
ASYNC_STATES: ['data', 'isLoading', 'error'],
|
|
26
|
+
STATE_MANAGEMENT: {
|
|
27
|
+
simple: 'useState',
|
|
28
|
+
complex: 'useReducer',
|
|
29
|
+
globalUI: 'Context API',
|
|
30
|
+
globalApp: 'Zustand',
|
|
31
|
+
server: 'TanStack Query'
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
export const applyQualityRulesDefinition = {
|
|
35
|
+
name: 'apply_quality_rules',
|
|
36
|
+
description: '규칙 적용|표준 적용|apply rules|apply standards|follow conventions|적용해 - Apply quality rules',
|
|
37
|
+
inputSchema: {
|
|
38
|
+
type: 'object',
|
|
39
|
+
properties: {
|
|
40
|
+
scope: { type: 'string', description: 'Application scope', enum: ['naming', 'structure', 'typescript', 'react', 'accessibility', 'all'] },
|
|
41
|
+
language: { type: 'string', description: 'Programming language context', enum: ['javascript', 'typescript', 'react', 'vue', 'general'] }
|
|
42
|
+
},
|
|
43
|
+
required: ['scope']
|
|
44
|
+
},
|
|
45
|
+
annotations: {
|
|
46
|
+
title: 'Apply Quality Rules',
|
|
47
|
+
audience: ['user', 'assistant']
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
export async function applyQualityRules(args) {
|
|
51
|
+
const { scope, language: contextLanguage = 'general' } = args;
|
|
52
|
+
const applicableRules = [];
|
|
53
|
+
if (scope === 'naming' || scope === 'all') {
|
|
54
|
+
applicableRules.push({
|
|
55
|
+
category: 'Naming Conventions',
|
|
56
|
+
rules: QUALITY_RULES.NAMING
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (scope === 'structure' || scope === 'all') {
|
|
60
|
+
applicableRules.push({
|
|
61
|
+
category: 'Code Structure',
|
|
62
|
+
rules: QUALITY_RULES.STRUCTURE
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (scope === 'typescript' || scope === 'all') {
|
|
66
|
+
applicableRules.push({
|
|
67
|
+
category: 'TypeScript Guidelines',
|
|
68
|
+
rules: QUALITY_RULES.ANTIPATTERNS.typescript
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (scope === 'react' || scope === 'all') {
|
|
72
|
+
applicableRules.push({
|
|
73
|
+
category: 'React Guidelines',
|
|
74
|
+
rules: QUALITY_RULES.ANTIPATTERNS.react
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (scope === 'accessibility' || scope === 'all') {
|
|
78
|
+
applicableRules.push({
|
|
79
|
+
category: 'Accessibility Guidelines',
|
|
80
|
+
rules: [
|
|
81
|
+
'Use semantic HTML elements',
|
|
82
|
+
'Provide alt text for images',
|
|
83
|
+
'Ensure keyboard navigation',
|
|
84
|
+
'Maintain color contrast ratios',
|
|
85
|
+
'Use ARIA labels when needed'
|
|
86
|
+
]
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
const qualityRulesResult = {
|
|
90
|
+
action: 'apply_quality_rules',
|
|
91
|
+
scope,
|
|
92
|
+
language: contextLanguage,
|
|
93
|
+
rules: applicableRules,
|
|
94
|
+
asyncStates: QUALITY_RULES.ASYNC_STATES,
|
|
95
|
+
stateManagement: QUALITY_RULES.STATE_MANAGEMENT,
|
|
96
|
+
status: 'success'
|
|
97
|
+
};
|
|
98
|
+
const rulesSummary = applicableRules.map(r => `${r.category}: ${Array.isArray(r.rules) ? r.rules.length + ' rules' : Object.keys(r.rules).length + ' items'}`).join(', ');
|
|
99
|
+
return {
|
|
100
|
+
content: [{ type: 'text', text: `Scope: ${scope}\nLanguage: ${contextLanguage}\nRules Applied: ${rulesSummary}\n\nAsync States: ${QUALITY_RULES.ASYNC_STATES.join(', ')}\n\nState Mgmt:\n${Object.entries(QUALITY_RULES.STATE_MANAGEMENT).map(([k, v]) => `- ${k}: ${v}`).join('\n')}` }]
|
|
101
|
+
};
|
|
102
|
+
}
|