project-graph-mcp 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.
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Project Guidelines and Instructions for AI Agents
3
+ */
4
+
5
+ export const AGENT_INSTRUCTIONS = `
6
+ # 🤖 Project Guidelines for AI Agents
7
+
8
+ ## 1. Architecture Standards (Symbiote.js)
9
+ - **Component Structure**: Always use Triple-File Partitioning for components:
10
+ - \`MyComponent.js\`: Class logic (extends Symbiote)
11
+ - \`MyComponent.tpl.js\`: HTML template (export template)
12
+ - \`MyComponent.css.js\`: CSS styles (export rootStyles/shadowStyles)
13
+ - **State Management**: Use \`this.init$\` for local state and \`this.sub()\` for reactivity.
14
+ - **Directives**: Use \`itemize\` for lists, \`js-d-kit\` for static generation.
15
+
16
+ ## 2. Test Annotations (@test/@expect)
17
+ Universal verification checklist system. Works for **any** test type.
18
+
19
+ ### Syntax
20
+ \`\`\`javascript
21
+ /**
22
+ * Method description
23
+ *
24
+ * @test {type}: {description}
25
+ * @expect {type}: {description}
26
+ */
27
+ async myMethod() { ... }
28
+ \`\`\`
29
+
30
+ ### @test Types by Category
31
+
32
+ #### 🌐 Browser / UI
33
+ | Type | Description | Example |
34
+ |------|-------------|---------|
35
+ | \`click\` | Click element | \`@test click: Click submit button\` |
36
+ | \`key\` | Keyboard input | \`@test key: Press Enter\` |
37
+ | \`drag\` | Drag and drop | \`@test drag: Drag item to list\` |
38
+ | \`type\` | Text input | \`@test type: Enter email in field\` |
39
+ | \`scroll\` | Scroll action | \`@test scroll: Scroll to bottom\` |
40
+ | \`hover\` | Mouse hover | \`@test hover: Hover over menu\` |
41
+
42
+ #### 🔌 API / Function
43
+ | Type | Description | Example |
44
+ |------|-------------|---------|
45
+ | \`request\` | HTTP request | \`@test request: POST /api/users\` |
46
+ | \`call\` | Function call | \`@test call: Call with valid params\` |
47
+ | \`invoke\` | Method invoke | \`@test invoke: Trigger event\` |
48
+ | \`mock\` | Mock setup | \`@test mock: Mock external service\` |
49
+
50
+ #### 💻 CLI / Process
51
+ | Type | Description | Example |
52
+ |------|-------------|---------|
53
+ | \`run\` | Run command | \`@test run: Run with --help flag\` |
54
+ | \`exec\` | Execute script | \`@test exec: Execute build script\` |
55
+ | \`spawn\` | Spawn process | \`@test spawn: Start server\` |
56
+ | \`input\` | Stdin input | \`@test input: Enter password\` |
57
+
58
+ #### 🔗 Integration / System
59
+ | Type | Description | Example |
60
+ |------|-------------|---------|
61
+ | \`setup\` | Test setup | \`@test setup: Create test database\` |
62
+ | \`action\` | Main action | \`@test action: Run migration\` |
63
+ | \`teardown\` | Cleanup | \`@test teardown: Remove temp files\` |
64
+ | \`wait\` | Wait condition | \`@test wait: Wait for DB connection\` |
65
+
66
+ ### @expect Types by Category
67
+
68
+ #### 🌐 Browser / UI
69
+ | Type | Description | Example |
70
+ |------|-------------|---------|
71
+ | \`attr\` | Attribute check | \`@expect attr: disabled attribute set\` |
72
+ | \`visual\` | Visual change | \`@expect visual: Button turns green\` |
73
+ | \`element\` | Element exists | \`@expect element: Modal appears\` |
74
+ | \`text\` | Text content | \`@expect text: Shows "Success"\` |
75
+
76
+ #### 🔌 API / Function
77
+ | Type | Description | Example |
78
+ |------|-------------|---------|
79
+ | \`status\` | HTTP status | \`@expect status: 201 Created\` |
80
+ | \`body\` | Response body | \`@expect body: Contains user ID\` |
81
+ | \`headers\` | Response headers | \`@expect headers: Content-Type JSON\` |
82
+ | \`error\` | Error thrown | \`@expect error: Throws ValidationError\` |
83
+
84
+ #### 💻 CLI / Process
85
+ | Type | Description | Example |
86
+ |------|-------------|---------|
87
+ | \`output\` | Stdout content | \`@expect output: Prints version\` |
88
+ | \`exitcode\` | Exit code | \`@expect exitcode: Returns 0\` |
89
+ | \`file\` | File created | \`@expect file: Creates config.json\` |
90
+ | \`stderr\` | Stderr content | \`@expect stderr: No errors\` |
91
+
92
+ #### 🔗 Integration / System
93
+ | Type | Description | Example |
94
+ |------|-------------|---------|
95
+ | \`state\` | State change | \`@expect state: User logged in\` |
96
+ | \`log\` | Log entry | \`@expect log: Info message logged\` |
97
+ | \`event\` | Event fired | \`@expect event: 'updated' emitted\` |
98
+ | \`db\` | Database change | \`@expect db: Row inserted\` |
99
+
100
+ ### Full Example
101
+ \`\`\`javascript
102
+ /**
103
+ * Create new user via API
104
+ *
105
+ * @test request: POST /api/users with valid data
106
+ * @test call: Validate email format
107
+ *
108
+ * @expect status: 201 Created
109
+ * @expect body: Contains user ID and email
110
+ * @expect db: User row created in database
111
+ * @expect event: 'user.created' event emitted
112
+ */
113
+ async createUser(data) {
114
+ // ...
115
+ }
116
+ \`\`\`
117
+
118
+ ## 3. General Coding Rules
119
+ - **ESM Only**: Use \`import\` / \`export\`. No \`require\`.
120
+ - **No Dependencies**: Avoid adding new npm packages unless critical.
121
+ - **Comments**: Write clear JSDoc for all public methods.
122
+ - **Async/Await**: Prefer async/await over promises.
123
+
124
+ ## 4. MCP Tools Usage
125
+ - **Graph**: Use \`get_skeleton\` first to map the codebase.
126
+ - **Deep Dive**: Use \`expand\` to read class details.
127
+ - **Tests**: Use \`get_pending_tests\` to see what needs verification.
128
+ - **Guidelines**: Use \`get_agent_instructions\` to refresh these rules.
129
+
130
+ ## 5. Custom Rules System
131
+ Configurable code analysis with auto-detection.
132
+
133
+ ### Available Tools
134
+ - \`get_custom_rules\`: List all rulesets and their rules
135
+ - \`set_custom_rule\`: Add or update a rule in a ruleset
136
+ - \`check_custom_rules\`: Run analysis (auto-detects applicable rulesets)
137
+
138
+ ### Auto-Detection
139
+ Rulesets are applied automatically based on:
140
+ 1. \`package.json\` dependencies
141
+ 2. Import patterns in source code
142
+ 3. Code patterns (e.g., \`extends Symbiote\`)
143
+
144
+ ### Creating New Rules
145
+ Use \`set_custom_rule\` to add framework-specific rules:
146
+ \`\`\`json
147
+ {
148
+ "ruleSet": "my-framework-2x",
149
+ "rule": {
150
+ "id": "my-rule-id",
151
+ "name": "Rule Name",
152
+ "description": "What this rule checks",
153
+ "pattern": "badPattern",
154
+ "patternType": "string",
155
+ "replacement": "Use goodPattern instead",
156
+ "severity": "warning",
157
+ "filePattern": "*.js",
158
+ "docs": "https://docs.example.com/rule"
159
+ }
160
+ }
161
+ \`\`\`
162
+
163
+ ### Severity Levels
164
+ - \`error\`: Critical issues that must be fixed
165
+ - \`warning\`: Important but not blocking
166
+ - \`info\`: Suggestions and best practices
167
+ `;
168
+
169
+ /**
170
+ * Get agent instructions
171
+ * @returns {string}
172
+ */
173
+ export function getInstructions() {
174
+ return AGENT_INSTRUCTIONS;
175
+ }
@@ -0,0 +1,214 @@
1
+ /**
2
+ * JSDoc Generator
3
+ * Auto-generates JSDoc templates from AST analysis
4
+ */
5
+
6
+ import { readFileSync } from 'fs';
7
+ import { relative } from 'path';
8
+ import { parse } from '../vendor/acorn.mjs';
9
+ import * as walk from '../vendor/walk.mjs';
10
+ import { getWorkspaceRoot } from './workspace.js';
11
+
12
+ /**
13
+ * @typedef {Object} JSDocTemplate
14
+ * @property {string} name - Function/method name
15
+ * @property {string} type - 'function' | 'method' | 'class'
16
+ * @property {string} file
17
+ * @property {number} line
18
+ * @property {string} jsdoc - Generated JSDoc template
19
+ */
20
+
21
+ /**
22
+ * Generate JSDoc for a single file
23
+ * @param {string} filePath - Absolute path to file
24
+ * @param {Object} [options]
25
+ * @param {boolean} [options.includeTests=true] - Include @test/@expect placeholders
26
+ * @returns {JSDocTemplate[]}
27
+ */
28
+ export function generateJSDoc(filePath, options = {}) {
29
+ const includeTests = options.includeTests !== false;
30
+ const results = [];
31
+
32
+ const code = readFileSync(filePath, 'utf-8');
33
+ const relPath = relative(getWorkspaceRoot(), filePath);
34
+
35
+ let ast;
36
+ try {
37
+ ast = parse(code, { ecmaVersion: 'latest', sourceType: 'module', locations: true });
38
+ } catch (e) {
39
+ return results;
40
+ }
41
+
42
+ // Check if line already has JSDoc
43
+ const hasJSDocAt = (line) => {
44
+ const lines = code.split('\n');
45
+ // Look backwards from function line for JSDoc closing */
46
+ for (let i = line - 2; i >= Math.max(0, line - 15); i--) {
47
+ const trimmed = lines[i]?.trim();
48
+ if (!trimmed) continue; // Skip empty lines
49
+ // Found JSDoc end - look for start
50
+ if (trimmed === '*/' || trimmed.endsWith('*/')) {
51
+ // Now look for /** opening above
52
+ for (let j = i - 1; j >= Math.max(0, i - 20); j--) {
53
+ const upper = lines[j]?.trim();
54
+ if (upper?.startsWith('/**')) return true;
55
+ // If we hit something non-JSDoc, stop
56
+ if (upper && !upper.startsWith('*')) break;
57
+ }
58
+ return false;
59
+ }
60
+ // If we hit code, stop
61
+ if (!trimmed.startsWith('*') && !trimmed.startsWith('//')) break;
62
+ }
63
+ return false;
64
+ };
65
+
66
+ walk.simple(ast, {
67
+ FunctionDeclaration(node) {
68
+ if (!node.id) return;
69
+ if (hasJSDocAt(node.loc.start.line)) return;
70
+
71
+ const jsdoc = buildJSDoc({
72
+ name: node.id.name,
73
+ params: node.params,
74
+ async: node.async,
75
+ includeTests,
76
+ });
77
+
78
+ results.push({
79
+ name: node.id.name,
80
+ type: 'function',
81
+ file: relPath,
82
+ line: node.loc.start.line,
83
+ jsdoc,
84
+ });
85
+ },
86
+
87
+ ClassDeclaration(node) {
88
+ if (!node.id) return;
89
+
90
+ // Check methods
91
+ for (const element of node.body.body) {
92
+ if (element.type === 'MethodDefinition') {
93
+ const methodName = element.key.name || element.key.value;
94
+
95
+ // Skip constructor, getters, setters, private
96
+ if (element.kind !== 'method') continue;
97
+ if (methodName.startsWith('_')) continue;
98
+ if (hasJSDocAt(element.loc.start.line)) continue;
99
+
100
+ const funcNode = element.value;
101
+ const jsdoc = buildJSDoc({
102
+ name: methodName,
103
+ params: funcNode.params,
104
+ async: funcNode.async,
105
+ includeTests,
106
+ });
107
+
108
+ results.push({
109
+ name: `${node.id.name}.${methodName}`,
110
+ type: 'method',
111
+ file: relPath,
112
+ line: element.loc.start.line,
113
+ jsdoc,
114
+ });
115
+ }
116
+ }
117
+ },
118
+ });
119
+
120
+ return results;
121
+ }
122
+
123
+ /**
124
+ * Build JSDoc string from function info
125
+ * @param {Object} info
126
+ * @param {string} info.name
127
+ * @param {Array} info.params
128
+ * @param {boolean} info.async
129
+ * @param {boolean} info.includeTests
130
+ * @returns {string}
131
+ */
132
+ function buildJSDoc(info) {
133
+ const lines = ['/**'];
134
+
135
+ // Description placeholder
136
+ lines.push(` * TODO: Add description for ${info.name}`);
137
+
138
+ // Parameters
139
+ for (const param of info.params) {
140
+ const paramName = extractParamName(param);
141
+ const paramType = inferParamType(param);
142
+ lines.push(` * @param {${paramType}} ${paramName}`);
143
+ }
144
+
145
+ // Return type
146
+ lines.push(` * @returns {${info.async ? 'Promise<*>' : '*'}}`);
147
+
148
+ // Test annotations (Agentic Verification)
149
+ if (info.includeTests) {
150
+ lines.push(` * @test TODO: describe test scenario`);
151
+ lines.push(` * @expect TODO: expected result`);
152
+ }
153
+
154
+ lines.push(' */');
155
+ return lines.join('\n');
156
+ }
157
+
158
+ /**
159
+ * Extract parameter name from AST node
160
+ * @param {Object} param
161
+ * @returns {string}
162
+ */
163
+ function extractParamName(param) {
164
+ if (param.type === 'Identifier') {
165
+ return param.name;
166
+ }
167
+ if (param.type === 'AssignmentPattern' && param.left.type === 'Identifier') {
168
+ return `[${param.left.name}]`; // Optional param
169
+ }
170
+ if (param.type === 'RestElement' && param.argument.type === 'Identifier') {
171
+ return `...${param.argument.name}`;
172
+ }
173
+ if (param.type === 'ObjectPattern') {
174
+ return 'options';
175
+ }
176
+ if (param.type === 'ArrayPattern') {
177
+ return 'args';
178
+ }
179
+ return 'param';
180
+ }
181
+
182
+ /**
183
+ * Infer parameter type from AST
184
+ * @param {Object} param
185
+ * @returns {string}
186
+ */
187
+ function inferParamType(param) {
188
+ if (param.type === 'AssignmentPattern') {
189
+ const defaultVal = param.right;
190
+ if (defaultVal.type === 'Literal') {
191
+ if (typeof defaultVal.value === 'string') return 'string';
192
+ if (typeof defaultVal.value === 'number') return 'number';
193
+ if (typeof defaultVal.value === 'boolean') return 'boolean';
194
+ }
195
+ if (defaultVal.type === 'ArrayExpression') return 'Array';
196
+ if (defaultVal.type === 'ObjectExpression') return 'Object';
197
+ }
198
+ if (param.type === 'RestElement') return 'Array';
199
+ if (param.type === 'ObjectPattern') return 'Object';
200
+ if (param.type === 'ArrayPattern') return 'Array';
201
+ return '*';
202
+ }
203
+
204
+ /**
205
+ * Generate JSDoc for specific function by name
206
+ * @param {string} filePath
207
+ * @param {string} functionName
208
+ * @param {Object} [options]
209
+ * @returns {JSDocTemplate|null}
210
+ */
211
+ export function generateJSDocFor(filePath, functionName, options = {}) {
212
+ const results = generateJSDoc(filePath, options);
213
+ return results.find(r => r.name === functionName || r.name.endsWith(`.${functionName}`)) || null;
214
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Large Files Analyzer
3
+ * Identifies files that may need splitting
4
+ */
5
+
6
+ import { readFileSync, readdirSync, statSync } from 'fs';
7
+ import { join, relative, resolve } from 'path';
8
+ import { parse } from '../vendor/acorn.mjs';
9
+ import * as walk from '../vendor/walk.mjs';
10
+ import { shouldExcludeDir, shouldExcludeFile, parseGitignore } from './filters.js';
11
+
12
+ /**
13
+ * @typedef {Object} LargeFileItem
14
+ * @property {string} file
15
+ * @property {number} lines
16
+ * @property {number} functions
17
+ * @property {number} classes
18
+ * @property {number} exports
19
+ * @property {string} rating - 'ok' | 'warning' | 'critical'
20
+ * @property {string[]} reasons
21
+ */
22
+
23
+ /**
24
+ * Find all JS files
25
+ * @param {string} dir
26
+ * @param {string} rootDir
27
+ * @returns {string[]}
28
+ */
29
+ function findJSFiles(dir, rootDir = dir) {
30
+ if (dir === rootDir) parseGitignore(rootDir);
31
+ const files = [];
32
+
33
+ try {
34
+ for (const entry of readdirSync(dir)) {
35
+ const fullPath = join(dir, entry);
36
+ const relativePath = relative(rootDir, fullPath);
37
+ const stat = statSync(fullPath);
38
+
39
+ if (stat.isDirectory()) {
40
+ if (!shouldExcludeDir(entry, relativePath)) {
41
+ files.push(...findJSFiles(fullPath, rootDir));
42
+ }
43
+ } else if (entry.endsWith('.js') && !entry.endsWith('.css.js') && !entry.endsWith('.tpl.js')) {
44
+ if (!shouldExcludeFile(entry, relativePath)) {
45
+ files.push(fullPath);
46
+ }
47
+ }
48
+ }
49
+ } catch (e) { }
50
+
51
+ return files;
52
+ }
53
+
54
+ /**
55
+ * Analyze a single file
56
+ * @param {string} filePath
57
+ * @returns {LargeFileItem}
58
+ */
59
+ function analyzeFile(filePath, rootDir) {
60
+ const code = readFileSync(filePath, 'utf-8');
61
+ const relPath = relative(rootDir, filePath);
62
+ const lines = code.split('\n').length;
63
+
64
+ let functions = 0;
65
+ let classes = 0;
66
+ let exports = 0;
67
+
68
+ let ast;
69
+ try {
70
+ ast = parse(code, { ecmaVersion: 'latest', sourceType: 'module', locations: true });
71
+ } catch (e) {
72
+ return { file: relPath, lines, functions: 0, classes: 0, exports: 0, rating: 'ok', reasons: [] };
73
+ }
74
+
75
+ walk.simple(ast, {
76
+ FunctionDeclaration() { functions++; },
77
+ ArrowFunctionExpression(node) {
78
+ if (node.body.type === 'BlockStatement') functions++;
79
+ },
80
+ ClassDeclaration() { classes++; },
81
+ ExportNamedDeclaration() { exports++; },
82
+ ExportDefaultDeclaration() { exports++; },
83
+ });
84
+
85
+ // Calculate rating
86
+ const reasons = [];
87
+ let score = 0;
88
+
89
+ if (lines > 500) {
90
+ score += 2;
91
+ reasons.push(`${lines} lines (>500)`);
92
+ } else if (lines > 300) {
93
+ score += 1;
94
+ reasons.push(`${lines} lines (>300)`);
95
+ }
96
+
97
+ if (functions > 15) {
98
+ score += 2;
99
+ reasons.push(`${functions} functions (>15)`);
100
+ } else if (functions > 10) {
101
+ score += 1;
102
+ reasons.push(`${functions} functions (>10)`);
103
+ }
104
+
105
+ if (classes > 3) {
106
+ score += 2;
107
+ reasons.push(`${classes} classes (>3)`);
108
+ } else if (classes > 1) {
109
+ score += 1;
110
+ reasons.push(`${classes} classes (>1)`);
111
+ }
112
+
113
+ if (exports > 10) {
114
+ score += 2;
115
+ reasons.push(`${exports} exports (>10)`);
116
+ } else if (exports > 5) {
117
+ score += 1;
118
+ reasons.push(`${exports} exports (>5)`);
119
+ }
120
+
121
+ let rating = 'ok';
122
+ if (score >= 4) rating = 'critical';
123
+ else if (score >= 2) rating = 'warning';
124
+
125
+ return { file: relPath, lines, functions, classes, exports, rating, reasons };
126
+ }
127
+
128
+ /**
129
+ * Get large files analysis
130
+ * @param {string} dir
131
+ * @param {Object} [options]
132
+ * @param {boolean} [options.onlyProblematic=false] - Only show warning/critical
133
+ * @returns {Promise<{total: number, stats: Object, items: LargeFileItem[]}>}
134
+ */
135
+ export async function getLargeFiles(dir, options = {}) {
136
+ const onlyProblematic = options.onlyProblematic || false;
137
+ const resolvedDir = resolve(dir);
138
+ const files = findJSFiles(dir);
139
+ let items = files.map(f => analyzeFile(f, resolvedDir));
140
+
141
+ if (onlyProblematic) {
142
+ items = items.filter(i => i.rating !== 'ok');
143
+ }
144
+
145
+ // Sort by lines descending
146
+ items.sort((a, b) => b.lines - a.lines);
147
+
148
+ const stats = {
149
+ totalFiles: files.length,
150
+ ok: items.filter(i => i.rating === 'ok').length,
151
+ warning: items.filter(i => i.rating === 'warning').length,
152
+ critical: items.filter(i => i.rating === 'critical').length,
153
+ totalLines: items.reduce((s, i) => s + i.lines, 0),
154
+ avgLines: items.length > 0 ? Math.round(items.reduce((s, i) => s + i.lines, 0) / items.length) : 0,
155
+ };
156
+
157
+ return {
158
+ total: items.length,
159
+ stats,
160
+ items: items.slice(0, 30),
161
+ };
162
+ }