project-graph-mcp 1.3.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.
- package/README.md +223 -17
- package/{AGENT_ROLE.md → docs/examples/AGENT_ROLE.md} +87 -30
- package/{AGENT_ROLE_MINIMAL.md → docs/examples/AGENT_ROLE_MINIMAL.md} +23 -8
- package/package.json +12 -8
- package/src/.project-graph-cache.json +1 -0
- package/src/analysis/analysis-cache.js +7 -0
- package/src/analysis/complexity.js +14 -0
- package/src/analysis/custom-rules.js +36 -0
- package/src/analysis/db-analysis.js +9 -0
- package/src/analysis/dead-code.js +19 -0
- package/src/analysis/full-analysis.js +18 -0
- package/src/analysis/jsdoc-checker.js +24 -0
- package/src/analysis/jsdoc-generator.js +10 -0
- package/src/analysis/large-files.js +11 -0
- package/src/analysis/outdated-patterns.js +12 -0
- package/src/analysis/similar-functions.js +16 -0
- package/src/analysis/test-annotations.js +21 -0
- package/src/analysis/type-checker.js +8 -0
- package/src/analysis/undocumented.js +14 -0
- package/src/cli/cli-handlers.js +4 -0
- package/src/cli/cli.js +5 -0
- package/src/compact/ai-context.js +7 -0
- package/src/compact/compact.js +18 -0
- package/src/compact/compress.js +13 -0
- package/src/compact/ctx-to-jsdoc.js +29 -0
- package/src/compact/doc-dialect.js +30 -0
- package/src/compact/expand.js +37 -0
- package/src/compact/framework-references.js +5 -0
- package/src/compact/instructions.js +3 -0
- package/src/compact/mode-config.js +8 -0
- package/src/compact/validate-pipeline.js +9 -0
- package/src/core/event-bus.js +9 -0
- package/src/core/filters.js +14 -0
- package/src/core/graph-builder.js +12 -0
- package/src/core/parser.js +31 -0
- package/src/core/workspace.js +8 -0
- package/src/lang/lang-go.js +17 -0
- package/src/lang/lang-python.js +12 -0
- package/src/lang/lang-sql.js +23 -0
- package/src/lang/lang-typescript.js +9 -0
- package/src/lang/lang-utils.js +4 -0
- package/src/mcp/mcp-server.js +17 -0
- package/src/mcp/tool-defs.js +3 -0
- package/src/mcp/tools.js +25 -0
- package/src/network/backend-lifecycle.js +19 -0
- package/src/network/backend.js +5 -0
- package/src/network/local-gateway.js +23 -0
- package/src/network/mdns.js +13 -0
- package/src/network/server.js +10 -0
- package/src/network/web-server.js +34 -0
- package/vendor/terser.mjs +49 -0
- package/web/.project-graph-cache.json +1 -0
- package/web/app.js +16 -0
- package/web/components/code-block.js +3 -0
- package/web/components/quick-open.js +5 -0
- package/web/dashboard-state.js +3 -0
- package/web/dashboard.html +27 -0
- package/web/dashboard.js +8 -0
- package/web/highlight.js +13 -0
- package/web/index.html +35 -0
- package/web/panels/ActionBoard/ActionBoard.css.js +1 -0
- package/web/panels/ActionBoard/ActionBoard.js +4 -0
- package/web/panels/ActionBoard/ActionBoard.tpl.js +1 -0
- package/web/panels/EventItem/EventItem.css.js +1 -0
- package/web/panels/EventItem/EventItem.js +4 -0
- package/web/panels/EventItem/EventItem.tpl.js +1 -0
- package/web/panels/ProjectItem/ProjectItem.css.js +1 -0
- package/web/panels/ProjectItem/ProjectItem.js +5 -0
- package/web/panels/ProjectItem/ProjectItem.tpl.js +1 -0
- package/web/panels/ProjectList/ProjectList.css.js +1 -0
- package/web/panels/ProjectList/ProjectList.js +4 -0
- package/web/panels/ProjectList/ProjectList.tpl.js +1 -0
- package/web/panels/SettingsPanel/.project-graph-cache.json +1 -0
- package/web/panels/SettingsPanel/SettingsPanel.css.js +1 -0
- package/web/panels/SettingsPanel/SettingsPanel.js +7 -0
- package/web/panels/SettingsPanel/SettingsPanel.tpl.js +1 -0
- package/web/panels/code-viewer.js +5 -0
- package/web/panels/ctx-panel.js +4 -0
- package/web/panels/dep-graph.js +6 -0
- package/web/panels/file-tree.js +188 -0
- package/web/panels/health-panel.js +3 -0
- package/web/panels/live-monitor.js +3 -0
- package/web/state.js +17 -0
- package/web/style.css +157 -0
- package/references/symbiote-3x.md +0 -834
- package/src/cli-handlers.js +0 -140
- package/src/cli.js +0 -83
- package/src/complexity.js +0 -223
- package/src/custom-rules.js +0 -583
- package/src/db-analysis.js +0 -194
- package/src/dead-code.js +0 -468
- package/src/filters.js +0 -227
- package/src/framework-references.js +0 -177
- package/src/full-analysis.js +0 -174
- package/src/graph-builder.js +0 -299
- package/src/instructions.js +0 -175
- package/src/jsdoc-generator.js +0 -214
- package/src/lang-go.js +0 -285
- package/src/lang-python.js +0 -197
- package/src/lang-sql.js +0 -309
- package/src/lang-typescript.js +0 -190
- package/src/lang-utils.js +0 -124
- package/src/large-files.js +0 -162
- package/src/mcp-server.js +0 -468
- package/src/outdated-patterns.js +0 -295
- package/src/parser.js +0 -452
- package/src/server.js +0 -28
- package/src/similar-functions.js +0 -278
- package/src/test-annotations.js +0 -301
- package/src/tool-defs.js +0 -525
- package/src/tools.js +0 -470
- package/src/undocumented.js +0 -260
- package/src/workspace.js +0 -70
package/src/similar-functions.js
DELETED
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Similar Functions Detector
|
|
3
|
-
* Finds functionally similar functions across the codebase
|
|
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} FunctionSignature
|
|
14
|
-
* @property {string} name
|
|
15
|
-
* @property {string} file
|
|
16
|
-
* @property {number} line
|
|
17
|
-
* @property {number} paramCount
|
|
18
|
-
* @property {string[]} paramNames
|
|
19
|
-
* @property {boolean} async
|
|
20
|
-
* @property {string} bodyHash - Structural hash of function body
|
|
21
|
-
* @property {string[]} calls - Functions called inside
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* @typedef {Object} SimilarPair
|
|
26
|
-
* @property {FunctionSignature} a
|
|
27
|
-
* @property {FunctionSignature} b
|
|
28
|
-
* @property {number} similarity - 0-100
|
|
29
|
-
* @property {string[]} reasons
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Find all JS files
|
|
34
|
-
* @param {string} dir
|
|
35
|
-
* @param {string} rootDir
|
|
36
|
-
* @returns {string[]}
|
|
37
|
-
*/
|
|
38
|
-
function findJSFiles(dir, rootDir = dir) {
|
|
39
|
-
if (dir === rootDir) parseGitignore(rootDir);
|
|
40
|
-
const files = [];
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
for (const entry of readdirSync(dir)) {
|
|
44
|
-
const fullPath = join(dir, entry);
|
|
45
|
-
const relativePath = relative(rootDir, fullPath);
|
|
46
|
-
const stat = statSync(fullPath);
|
|
47
|
-
|
|
48
|
-
if (stat.isDirectory()) {
|
|
49
|
-
if (!shouldExcludeDir(entry, relativePath)) {
|
|
50
|
-
files.push(...findJSFiles(fullPath, rootDir));
|
|
51
|
-
}
|
|
52
|
-
} else if (entry.endsWith('.js') && !entry.endsWith('.css.js') && !entry.endsWith('.tpl.js')) {
|
|
53
|
-
if (!shouldExcludeFile(entry, relativePath)) {
|
|
54
|
-
files.push(fullPath);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
} catch (e) { }
|
|
59
|
-
|
|
60
|
-
return files;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Extract function signatures from a file
|
|
65
|
-
* @param {string} filePath
|
|
66
|
-
* @returns {FunctionSignature[]}
|
|
67
|
-
*/
|
|
68
|
-
function extractSignatures(filePath, rootDir) {
|
|
69
|
-
const code = readFileSync(filePath, 'utf-8');
|
|
70
|
-
const relPath = relative(rootDir, filePath);
|
|
71
|
-
const signatures = [];
|
|
72
|
-
|
|
73
|
-
let ast;
|
|
74
|
-
try {
|
|
75
|
-
ast = parse(code, { ecmaVersion: 'latest', sourceType: 'module', locations: true });
|
|
76
|
-
} catch (e) {
|
|
77
|
-
return signatures;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
walk.simple(ast, {
|
|
81
|
-
FunctionDeclaration(node) {
|
|
82
|
-
if (!node.id) return;
|
|
83
|
-
signatures.push(buildSignature(node, node.id.name, relPath));
|
|
84
|
-
},
|
|
85
|
-
|
|
86
|
-
MethodDefinition(node) {
|
|
87
|
-
if (node.kind !== 'method') return;
|
|
88
|
-
const name = node.key.name || node.key.value;
|
|
89
|
-
if (name.startsWith('_')) return;
|
|
90
|
-
signatures.push(buildSignature(node.value, name, relPath));
|
|
91
|
-
},
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
return signatures;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Build signature from function node
|
|
99
|
-
* @param {Object} node
|
|
100
|
-
* @param {string} name
|
|
101
|
-
* @param {string} file
|
|
102
|
-
* @returns {FunctionSignature}
|
|
103
|
-
*/
|
|
104
|
-
function buildSignature(node, name, file) {
|
|
105
|
-
const paramNames = node.params.map(p => extractParamName(p));
|
|
106
|
-
const calls = [];
|
|
107
|
-
|
|
108
|
-
// Extract function calls
|
|
109
|
-
walk.simple(node.body, {
|
|
110
|
-
CallExpression(callNode) {
|
|
111
|
-
if (callNode.callee.type === 'Identifier') {
|
|
112
|
-
calls.push(callNode.callee.name);
|
|
113
|
-
} else if (callNode.callee.type === 'MemberExpression' && callNode.callee.property.type === 'Identifier') {
|
|
114
|
-
calls.push(callNode.callee.property.name);
|
|
115
|
-
}
|
|
116
|
-
},
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
// Create structural hash
|
|
120
|
-
const bodyHash = hashBodyStructure(node.body);
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
name,
|
|
124
|
-
file,
|
|
125
|
-
line: node.loc?.start?.line || 0,
|
|
126
|
-
paramCount: node.params.length,
|
|
127
|
-
paramNames,
|
|
128
|
-
async: node.async || false,
|
|
129
|
-
bodyHash,
|
|
130
|
-
calls: [...new Set(calls)],
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Extract param name
|
|
136
|
-
* @param {Object} param
|
|
137
|
-
* @returns {string}
|
|
138
|
-
*/
|
|
139
|
-
function extractParamName(param) {
|
|
140
|
-
if (param.type === 'Identifier') return param.name;
|
|
141
|
-
if (param.type === 'AssignmentPattern' && param.left.type === 'Identifier') return param.left.name;
|
|
142
|
-
if (param.type === 'RestElement' && param.argument.type === 'Identifier') return param.argument.name;
|
|
143
|
-
return 'param';
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Create structural hash of function body
|
|
148
|
-
* @param {Object} body
|
|
149
|
-
* @returns {string}
|
|
150
|
-
*/
|
|
151
|
-
function hashBodyStructure(body) {
|
|
152
|
-
const structure = [];
|
|
153
|
-
|
|
154
|
-
walk.simple(body, {
|
|
155
|
-
IfStatement() { structure.push('IF'); },
|
|
156
|
-
ForStatement() { structure.push('FOR'); },
|
|
157
|
-
ForOfStatement() { structure.push('FOROF'); },
|
|
158
|
-
ForInStatement() { structure.push('FORIN'); },
|
|
159
|
-
WhileStatement() { structure.push('WHILE'); },
|
|
160
|
-
SwitchStatement() { structure.push('SWITCH'); },
|
|
161
|
-
TryStatement() { structure.push('TRY'); },
|
|
162
|
-
ReturnStatement() { structure.push('RET'); },
|
|
163
|
-
ThrowStatement() { structure.push('THROW'); },
|
|
164
|
-
AwaitExpression() { structure.push('AWAIT'); },
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
return structure.join('|');
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Calculate similarity between two functions
|
|
172
|
-
* @param {FunctionSignature} a
|
|
173
|
-
* @param {FunctionSignature} b
|
|
174
|
-
* @returns {{similarity: number, reasons: string[]}}
|
|
175
|
-
*/
|
|
176
|
-
function calculateSimilarity(a, b) {
|
|
177
|
-
const reasons = [];
|
|
178
|
-
let score = 0;
|
|
179
|
-
let maxScore = 0;
|
|
180
|
-
|
|
181
|
-
// Same param count (important)
|
|
182
|
-
maxScore += 30;
|
|
183
|
-
if (a.paramCount === b.paramCount) {
|
|
184
|
-
score += 30;
|
|
185
|
-
reasons.push('Same param count');
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Similar param names
|
|
189
|
-
maxScore += 20;
|
|
190
|
-
const commonParams = a.paramNames.filter(p => b.paramNames.includes(p));
|
|
191
|
-
if (commonParams.length > 0 && a.paramNames.length > 0) {
|
|
192
|
-
const paramSim = commonParams.length / Math.max(a.paramNames.length, b.paramNames.length);
|
|
193
|
-
score += Math.round(paramSim * 20);
|
|
194
|
-
if (paramSim >= 0.5) reasons.push(`Similar params: ${commonParams.join(', ')}`);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Same async status
|
|
198
|
-
maxScore += 10;
|
|
199
|
-
if (a.async === b.async) {
|
|
200
|
-
score += 10;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Similar body structure
|
|
204
|
-
maxScore += 25;
|
|
205
|
-
if (a.bodyHash === b.bodyHash && a.bodyHash.length > 0) {
|
|
206
|
-
score += 25;
|
|
207
|
-
reasons.push('Identical structure');
|
|
208
|
-
} else if (a.bodyHash.length > 0 && b.bodyHash.length > 0) {
|
|
209
|
-
const aTokens = a.bodyHash.split('|');
|
|
210
|
-
const bTokens = b.bodyHash.split('|');
|
|
211
|
-
const commonTokens = aTokens.filter(t => bTokens.includes(t));
|
|
212
|
-
if (commonTokens.length > 0) {
|
|
213
|
-
const structSim = commonTokens.length / Math.max(aTokens.length, bTokens.length);
|
|
214
|
-
score += Math.round(structSim * 25);
|
|
215
|
-
if (structSim >= 0.5) reasons.push('Similar control flow');
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Common function calls
|
|
220
|
-
maxScore += 15;
|
|
221
|
-
const commonCalls = a.calls.filter(c => b.calls.includes(c));
|
|
222
|
-
if (commonCalls.length > 0 && a.calls.length > 0 && b.calls.length > 0) {
|
|
223
|
-
const callSim = commonCalls.length / Math.max(a.calls.length, b.calls.length);
|
|
224
|
-
score += Math.round(callSim * 15);
|
|
225
|
-
if (commonCalls.length >= 2) reasons.push(`Common calls: ${commonCalls.slice(0, 3).join(', ')}`);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const similarity = Math.round((score / maxScore) * 100);
|
|
229
|
-
return { similarity, reasons };
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Get similar functions in directory
|
|
234
|
-
* @param {string} dir
|
|
235
|
-
* @param {Object} [options]
|
|
236
|
-
* @param {number} [options.threshold=60] - Minimum similarity percentage
|
|
237
|
-
* @returns {Promise<{total: number, pairs: SimilarPair[]}>}
|
|
238
|
-
*/
|
|
239
|
-
export async function getSimilarFunctions(dir, options = {}) {
|
|
240
|
-
const threshold = options.threshold || 60;
|
|
241
|
-
const resolvedDir = resolve(dir);
|
|
242
|
-
const files = findJSFiles(dir);
|
|
243
|
-
const allSignatures = [];
|
|
244
|
-
|
|
245
|
-
// Collect all signatures
|
|
246
|
-
for (const file of files) {
|
|
247
|
-
allSignatures.push(...extractSignatures(file, resolvedDir));
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Compare all pairs
|
|
251
|
-
const pairs = [];
|
|
252
|
-
for (let i = 0; i < allSignatures.length; i++) {
|
|
253
|
-
for (let j = i + 1; j < allSignatures.length; j++) {
|
|
254
|
-
const a = allSignatures[i];
|
|
255
|
-
const b = allSignatures[j];
|
|
256
|
-
|
|
257
|
-
// Skip same file same name (likely intentional overload)
|
|
258
|
-
if (a.file === b.file && a.name === b.name) continue;
|
|
259
|
-
|
|
260
|
-
// Skip very small functions
|
|
261
|
-
if (a.bodyHash.length < 3 && b.bodyHash.length < 3) continue;
|
|
262
|
-
|
|
263
|
-
const { similarity, reasons } = calculateSimilarity(a, b);
|
|
264
|
-
|
|
265
|
-
if (similarity >= threshold && reasons.length > 0) {
|
|
266
|
-
pairs.push({ a, b, similarity, reasons });
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// Sort by similarity descending
|
|
272
|
-
pairs.sort((x, y) => y.similarity - x.similarity);
|
|
273
|
-
|
|
274
|
-
return {
|
|
275
|
-
total: pairs.length,
|
|
276
|
-
pairs: pairs.slice(0, 20),
|
|
277
|
-
};
|
|
278
|
-
}
|
package/src/test-annotations.js
DELETED
|
@@ -1,301 +0,0 @@
|
|
|
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
|
-
}
|