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.
- package/AGENT_ROLE.md +126 -0
- package/AGENT_ROLE_MINIMAL.md +54 -0
- package/CONFIGURATION.md +188 -0
- package/LICENSE +21 -0
- package/README.md +279 -0
- package/package.json +46 -0
- package/references/symbiote-3x.md +834 -0
- package/rules/express-5.json +76 -0
- package/rules/fastify-5.json +75 -0
- package/rules/nestjs-10.json +88 -0
- package/rules/nextjs-15.json +87 -0
- package/rules/node-22.json +156 -0
- package/rules/react-18.json +87 -0
- package/rules/react-19.json +76 -0
- package/rules/symbiote-2x.json +158 -0
- package/rules/symbiote-3x.json +221 -0
- package/rules/typescript-5.json +69 -0
- package/rules/vue-3.json +79 -0
- package/src/cli-handlers.js +140 -0
- package/src/cli.js +83 -0
- package/src/complexity.js +223 -0
- package/src/custom-rules.js +583 -0
- package/src/dead-code.js +468 -0
- package/src/filters.js +226 -0
- package/src/framework-references.js +177 -0
- package/src/full-analysis.js +159 -0
- package/src/graph-builder.js +269 -0
- package/src/instructions.js +175 -0
- package/src/jsdoc-generator.js +214 -0
- package/src/large-files.js +162 -0
- package/src/mcp-server.js +375 -0
- package/src/outdated-patterns.js +295 -0
- package/src/parser.js +293 -0
- package/src/server.js +28 -0
- package/src/similar-functions.js +278 -0
- package/src/test-annotations.js +301 -0
- package/src/tool-defs.js +444 -0
- package/src/tools.js +240 -0
- package/src/undocumented.js +260 -0
- package/src/workspace.js +70 -0
- package/vendor/acorn.mjs +6145 -0
- package/vendor/walk.mjs +437 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Annotations Parser
|
|
3
|
+
* Extracts @test/@expect JSDoc annotations for browser testing
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync, readdirSync, statSync, writeFileSync } from 'fs';
|
|
7
|
+
import { join, basename, relative, resolve } from 'path';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} TestStep
|
|
11
|
+
* @property {string} id - Unique ID (e.g., "togglePin.1")
|
|
12
|
+
* @property {string} type - Action type (click, key, drag, etc.)
|
|
13
|
+
* @property {string} description - What to do
|
|
14
|
+
* @property {boolean} completed - Whether test passed
|
|
15
|
+
* @property {string} [failReason] - Why it failed (if failed)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {Object} Feature
|
|
20
|
+
* @property {string} name - Method name
|
|
21
|
+
* @property {string} description - What the method does
|
|
22
|
+
* @property {TestStep[]} tests - Test steps
|
|
23
|
+
* @property {Array<{type: string, description: string}>} expects - Expected outcomes
|
|
24
|
+
* @property {string} file - Source file
|
|
25
|
+
* @property {number} line - Line number
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
// In-memory state for test progress
|
|
29
|
+
const testState = new Map();
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Parse @test/@expect annotations from a file
|
|
33
|
+
* @param {string} content
|
|
34
|
+
* @param {string} filePath
|
|
35
|
+
* @returns {Feature[]}
|
|
36
|
+
*/
|
|
37
|
+
export function parseAnnotations(content, filePath) {
|
|
38
|
+
const results = [];
|
|
39
|
+
const blockRegex = /\/\*\*([^]*?)\*\//g;
|
|
40
|
+
|
|
41
|
+
let match;
|
|
42
|
+
while ((match = blockRegex.exec(content)) !== null) {
|
|
43
|
+
const block = match[1];
|
|
44
|
+
|
|
45
|
+
// Check if block has @test or @expect
|
|
46
|
+
if (!block.includes('@test') && !block.includes('@expect')) continue;
|
|
47
|
+
|
|
48
|
+
// Find method name after the block
|
|
49
|
+
// Supports: methodName( | propName: ( | propName: async (
|
|
50
|
+
const afterBlock = content.slice(match.index + match[0].length);
|
|
51
|
+
const methodMatch = afterBlock.match(
|
|
52
|
+
/^\s*(?:async\s+)?(\w+)\s*\(/ // class method: methodName(
|
|
53
|
+
) || afterBlock.match(
|
|
54
|
+
/^\s*(\w+)\s*:\s*(?:async\s*)?\(/ // arrow in object: propName: (
|
|
55
|
+
) || afterBlock.match(
|
|
56
|
+
/^\s*(\w+)\s*:\s*(?:async\s+)?\(/ // arrow in object: propName: async (
|
|
57
|
+
);
|
|
58
|
+
if (!methodMatch) continue;
|
|
59
|
+
|
|
60
|
+
const methodName = methodMatch[1];
|
|
61
|
+
|
|
62
|
+
// Extract description (first line)
|
|
63
|
+
const descMatch = block.match(/^\s*\*\s*([^@\n][^\n]*)/m);
|
|
64
|
+
const description = descMatch ? descMatch[1].trim() : methodName;
|
|
65
|
+
|
|
66
|
+
// Extract @test annotations with unique IDs
|
|
67
|
+
const tests = [];
|
|
68
|
+
const testRegex = /@test\s+(\w+):\s*(.+)/g;
|
|
69
|
+
let testMatch;
|
|
70
|
+
let testIndex = 0;
|
|
71
|
+
while ((testMatch = testRegex.exec(block)) !== null) {
|
|
72
|
+
tests.push({
|
|
73
|
+
id: `${methodName}.${testIndex++}`,
|
|
74
|
+
type: testMatch[1],
|
|
75
|
+
description: testMatch[2].trim(),
|
|
76
|
+
completed: false,
|
|
77
|
+
failReason: null,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Extract @expect annotations
|
|
82
|
+
const expects = [];
|
|
83
|
+
const expectRegex = /@expect\s+(\w+):\s*(.+)/g;
|
|
84
|
+
let expectMatch;
|
|
85
|
+
while ((expectMatch = expectRegex.exec(block)) !== null) {
|
|
86
|
+
expects.push({
|
|
87
|
+
type: expectMatch[1],
|
|
88
|
+
description: expectMatch[2].trim(),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (tests.length || expects.length) {
|
|
93
|
+
const lineNumber = content.slice(0, match.index).split('\n').length;
|
|
94
|
+
|
|
95
|
+
results.push({
|
|
96
|
+
name: methodName,
|
|
97
|
+
description,
|
|
98
|
+
tests,
|
|
99
|
+
expects,
|
|
100
|
+
file: filePath,
|
|
101
|
+
line: lineNumber,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return results;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Find all JS files in directory
|
|
111
|
+
* @param {string} dir
|
|
112
|
+
* @returns {string[]}
|
|
113
|
+
*/
|
|
114
|
+
function findJSFiles(dir) {
|
|
115
|
+
const files = [];
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
for (const entry of readdirSync(dir)) {
|
|
119
|
+
const fullPath = join(dir, entry);
|
|
120
|
+
const stat = statSync(fullPath);
|
|
121
|
+
|
|
122
|
+
if (stat.isDirectory() && !entry.startsWith('.') && entry !== 'node_modules') {
|
|
123
|
+
files.push(...findJSFiles(fullPath));
|
|
124
|
+
} else if (entry.endsWith('.js') && !entry.endsWith('.css.js') && !entry.endsWith('.tpl.js')) {
|
|
125
|
+
files.push(fullPath);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} catch (e) {
|
|
129
|
+
// Directory not found
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return files;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get all features from a directory
|
|
137
|
+
* @param {string} dir
|
|
138
|
+
* @returns {Feature[]}
|
|
139
|
+
*/
|
|
140
|
+
export function getAllFeatures(dir) {
|
|
141
|
+
const files = findJSFiles(dir);
|
|
142
|
+
const features = [];
|
|
143
|
+
|
|
144
|
+
for (const file of files) {
|
|
145
|
+
const content = readFileSync(file, 'utf-8');
|
|
146
|
+
const parsed = parseAnnotations(content, file);
|
|
147
|
+
features.push(...parsed);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return features;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get pending (uncompleted) tests
|
|
155
|
+
* @param {string} dir
|
|
156
|
+
* @returns {TestStep[]}
|
|
157
|
+
*/
|
|
158
|
+
export function getPendingTests(dir) {
|
|
159
|
+
const resolvedDir = resolve(dir);
|
|
160
|
+
const features = getAllFeatures(dir);
|
|
161
|
+
const pending = [];
|
|
162
|
+
|
|
163
|
+
for (const feature of features) {
|
|
164
|
+
for (const test of feature.tests) {
|
|
165
|
+
const state = testState.get(test.id);
|
|
166
|
+
if (!state || !state.completed) {
|
|
167
|
+
pending.push({
|
|
168
|
+
...test,
|
|
169
|
+
feature: feature.name,
|
|
170
|
+
file: relative(resolvedDir, feature.file),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return pending;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Mark a test as passed
|
|
181
|
+
* @param {string} testId
|
|
182
|
+
*/
|
|
183
|
+
export function markTestPassed(testId) {
|
|
184
|
+
testState.set(testId, { completed: true, passed: true });
|
|
185
|
+
return { success: true, testId };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Mark a test as failed
|
|
190
|
+
* @param {string} testId
|
|
191
|
+
* @param {string} reason
|
|
192
|
+
*/
|
|
193
|
+
export function markTestFailed(testId, reason) {
|
|
194
|
+
testState.set(testId, { completed: true, passed: false, reason });
|
|
195
|
+
return { success: true, testId, reason };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get test summary
|
|
200
|
+
* @param {string} dir
|
|
201
|
+
* @returns {Object}
|
|
202
|
+
*/
|
|
203
|
+
export function getTestSummary(dir) {
|
|
204
|
+
const features = getAllFeatures(dir);
|
|
205
|
+
|
|
206
|
+
let total = 0;
|
|
207
|
+
let passed = 0;
|
|
208
|
+
let failed = 0;
|
|
209
|
+
let pending = 0;
|
|
210
|
+
const failures = [];
|
|
211
|
+
|
|
212
|
+
for (const feature of features) {
|
|
213
|
+
for (const test of feature.tests) {
|
|
214
|
+
total++;
|
|
215
|
+
const state = testState.get(test.id);
|
|
216
|
+
|
|
217
|
+
if (!state || !state.completed) {
|
|
218
|
+
pending++;
|
|
219
|
+
} else if (state.passed) {
|
|
220
|
+
passed++;
|
|
221
|
+
} else {
|
|
222
|
+
failed++;
|
|
223
|
+
failures.push({
|
|
224
|
+
id: test.id,
|
|
225
|
+
reason: state.reason,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
total,
|
|
233
|
+
passed,
|
|
234
|
+
failed,
|
|
235
|
+
pending,
|
|
236
|
+
progress: total > 0 ? Math.round((passed + failed) / total * 100) : 0,
|
|
237
|
+
failures,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Reset test state
|
|
243
|
+
*/
|
|
244
|
+
export function resetTestState() {
|
|
245
|
+
testState.clear();
|
|
246
|
+
return { success: true };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Generate markdown checklist
|
|
251
|
+
* @param {Feature[]} features
|
|
252
|
+
* @returns {string}
|
|
253
|
+
*/
|
|
254
|
+
export function generateMarkdown(features) {
|
|
255
|
+
const lines = [
|
|
256
|
+
'# Browser Test Checklist',
|
|
257
|
+
'',
|
|
258
|
+
`> Auto-generated from JSDoc @test/@expect annotations`,
|
|
259
|
+
`> Generated: ${new Date().toISOString().split('T')[0]}`,
|
|
260
|
+
'',
|
|
261
|
+
];
|
|
262
|
+
|
|
263
|
+
// Group by file
|
|
264
|
+
const byFile = {};
|
|
265
|
+
for (const feature of features) {
|
|
266
|
+
const key = feature.file;
|
|
267
|
+
if (!byFile[key]) byFile[key] = [];
|
|
268
|
+
byFile[key].push(feature);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
for (const [file, fileFeatures] of Object.entries(byFile)) {
|
|
272
|
+
lines.push(`## ${basename(file, '.js')}`);
|
|
273
|
+
lines.push('');
|
|
274
|
+
|
|
275
|
+
for (const feature of fileFeatures) {
|
|
276
|
+
lines.push(`### ${feature.name}()`);
|
|
277
|
+
lines.push(`${feature.description}`);
|
|
278
|
+
lines.push('');
|
|
279
|
+
|
|
280
|
+
if (feature.tests.length) {
|
|
281
|
+
lines.push('**Steps:**');
|
|
282
|
+
for (const test of feature.tests) {
|
|
283
|
+
const state = testState.get(test.id);
|
|
284
|
+
const check = state?.passed ? '[x]' : '[ ]';
|
|
285
|
+
lines.push(`- ${check} \`${test.type}\`: ${test.description}`);
|
|
286
|
+
}
|
|
287
|
+
lines.push('');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (feature.expects.length) {
|
|
291
|
+
lines.push('**Expected:**');
|
|
292
|
+
for (const expect of feature.expects) {
|
|
293
|
+
lines.push(`- ✅ \`${expect.type}\`: ${expect.description}`);
|
|
294
|
+
}
|
|
295
|
+
lines.push('');
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return lines.join('\n');
|
|
301
|
+
}
|