project-graph-mcp 1.3.0 → 1.5.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 +87 -30
- package/AGENT_ROLE_MINIMAL.md +23 -8
- package/README.md +100 -14
- package/package.json +1 -1
- package/src/.project-graph-cache.json +1 -0
- package/src/ai-context.js +113 -0
- package/src/analysis-cache.js +155 -0
- package/src/cli-handlers.js +131 -0
- package/src/cli.js +14 -2
- package/src/compact.js +207 -0
- package/src/complexity.js +21 -7
- package/src/compress.js +319 -0
- package/src/ctx-to-jsdoc.js +514 -0
- package/src/custom-rules.js +1 -0
- package/src/doc-dialect.js +716 -0
- package/src/full-analysis.js +307 -11
- package/src/instructions.js +3 -105
- package/src/jsdoc-checker.js +351 -0
- package/src/jsdoc-generator.js +0 -11
- package/src/large-files.js +1 -0
- package/src/mcp-server.js +208 -1
- package/src/mode-config.js +127 -0
- package/src/outdated-patterns.js +1 -0
- package/src/parser.js +223 -13
- package/src/similar-functions.js +1 -0
- package/src/test-annotations.js +203 -181
- package/src/tool-defs.js +270 -2
- package/src/tools.js +1 -1
- package/src/type-checker.js +188 -0
- package/src/undocumented.js +11 -12
- package/src/workspace.js +1 -1
- package/vendor/terser.mjs +49 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSDoc Consistency Checker (AST-based)
|
|
3
|
+
* Validates JSDoc annotations against actual function signatures
|
|
4
|
+
*
|
|
5
|
+
* Checks:
|
|
6
|
+
* - Param count mismatch (JSDoc vs AST)
|
|
7
|
+
* - Param name mismatch
|
|
8
|
+
* - Missing @returns on functions with return statements
|
|
9
|
+
* - Type hint inconsistency (default value vs JSDoc type)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync, readdirSync, statSync } from 'fs';
|
|
13
|
+
import { join, relative, resolve } from 'path';
|
|
14
|
+
import { parse } from '../vendor/acorn.mjs';
|
|
15
|
+
import * as walk from '../vendor/walk.mjs';
|
|
16
|
+
import { shouldExcludeDir, shouldExcludeFile, parseGitignore } from './filters.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {Object} JSDocIssue
|
|
20
|
+
* @property {string} file
|
|
21
|
+
* @property {number} line
|
|
22
|
+
* @property {string} name - Function or method name
|
|
23
|
+
* @property {'error'|'warning'} severity
|
|
24
|
+
* @property {string} message
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Find all JS files in directory
|
|
29
|
+
* @param {string} dir
|
|
30
|
+
* @param {string} rootDir
|
|
31
|
+
* @returns {string[]}
|
|
32
|
+
*/
|
|
33
|
+
function findJSFiles(dir, rootDir = dir) {
|
|
34
|
+
if (dir === rootDir) parseGitignore(rootDir);
|
|
35
|
+
const files = [];
|
|
36
|
+
try {
|
|
37
|
+
for (const entry of readdirSync(dir)) {
|
|
38
|
+
const fullPath = join(dir, entry);
|
|
39
|
+
const relativePath = relative(rootDir, fullPath);
|
|
40
|
+
const stat = statSync(fullPath);
|
|
41
|
+
if (stat.isDirectory()) {
|
|
42
|
+
if (!shouldExcludeDir(entry, relativePath)) {
|
|
43
|
+
files.push(...findJSFiles(fullPath, rootDir));
|
|
44
|
+
}
|
|
45
|
+
} else if (entry.endsWith('.js') && !entry.endsWith('.css.js') && !entry.endsWith('.tpl.js')) {
|
|
46
|
+
if (!shouldExcludeFile(entry, relativePath)) {
|
|
47
|
+
files.push(fullPath);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} catch (e) { /* dir not found */ }
|
|
52
|
+
return files;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Extract JSDoc comments with positions
|
|
57
|
+
* @param {string} code
|
|
58
|
+
* @returns {Array<{text: string, endLine: number, params: Array<{name: string, type: string}>, hasReturns: boolean}>}
|
|
59
|
+
*/
|
|
60
|
+
function extractJSDocComments(code) {
|
|
61
|
+
const comments = [];
|
|
62
|
+
const regex = /\/\*\*[\s\S]*?\*\//g;
|
|
63
|
+
let match;
|
|
64
|
+
|
|
65
|
+
while ((match = regex.exec(code)) !== null) {
|
|
66
|
+
const text = match[0];
|
|
67
|
+
const endLine = code.slice(0, match.index + text.length).split('\n').length;
|
|
68
|
+
|
|
69
|
+
// Parse @param tags — handle nested braces in types like {Array<{text: string}>}
|
|
70
|
+
const params = [];
|
|
71
|
+
const paramStartRegex = /@param\s+\{/g;
|
|
72
|
+
let paramStart;
|
|
73
|
+
while ((paramStart = paramStartRegex.exec(text)) !== null) {
|
|
74
|
+
// Find matching closing brace (balanced)
|
|
75
|
+
let depth = 1;
|
|
76
|
+
let i = paramStart.index + paramStart[0].length;
|
|
77
|
+
while (i < text.length && depth > 0) {
|
|
78
|
+
if (text[i] === '{') depth++;
|
|
79
|
+
else if (text[i] === '}') depth--;
|
|
80
|
+
i++;
|
|
81
|
+
}
|
|
82
|
+
if (depth !== 0) continue;
|
|
83
|
+
const type = text.slice(paramStart.index + paramStart[0].length, i - 1);
|
|
84
|
+
// Extract param name after the closing brace
|
|
85
|
+
const afterType = text.slice(i);
|
|
86
|
+
const nameMatch = afterType.match(/^\s+(\[?\w+(?:\.\w+)*\]?)/);
|
|
87
|
+
if (!nameMatch) continue;
|
|
88
|
+
let name = nameMatch[1];
|
|
89
|
+
// Strip [] from optional params: [opts] → opts
|
|
90
|
+
if (name.startsWith('[')) name = name.slice(1);
|
|
91
|
+
if (name.endsWith(']')) name = name.slice(0, -1);
|
|
92
|
+
// Strip dotted paths: options.includeTests → skip (nested property)
|
|
93
|
+
if (name.includes('.')) continue;
|
|
94
|
+
params.push({ name, type });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const hasReturns = /@returns?\s/.test(text);
|
|
98
|
+
|
|
99
|
+
comments.push({ text, endLine, params, hasReturns });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return comments;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Find JSDoc comment before a target line
|
|
107
|
+
* @param {Array} comments
|
|
108
|
+
* @param {number} targetLine
|
|
109
|
+
* @returns {Object|null}
|
|
110
|
+
*/
|
|
111
|
+
function findJSDocBefore(comments, targetLine) {
|
|
112
|
+
for (const comment of comments) {
|
|
113
|
+
const gap = targetLine - comment.endLine;
|
|
114
|
+
if (gap >= 0 && gap <= 2) return comment;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Extract parameter name from AST node
|
|
121
|
+
* @param {Object} param
|
|
122
|
+
* @returns {string}
|
|
123
|
+
*/
|
|
124
|
+
function extractParamName(param) {
|
|
125
|
+
if (param.type === 'Identifier') return param.name;
|
|
126
|
+
if (param.type === 'AssignmentPattern' && param.left.type === 'Identifier') return param.left.name;
|
|
127
|
+
if (param.type === 'RestElement' && param.argument.type === 'Identifier') return param.argument.name;
|
|
128
|
+
if (param.type === 'ObjectPattern') return 'options';
|
|
129
|
+
if (param.type === 'ArrayPattern') return 'args';
|
|
130
|
+
return 'param';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Infer expected type from AST default value
|
|
135
|
+
* @param {Object} param
|
|
136
|
+
* @returns {string|null}
|
|
137
|
+
*/
|
|
138
|
+
function inferTypeFromDefault(param) {
|
|
139
|
+
if (param.type !== 'AssignmentPattern') return null;
|
|
140
|
+
const def = param.right;
|
|
141
|
+
if (def.type === 'Literal') {
|
|
142
|
+
if (typeof def.value === 'string') return 'string';
|
|
143
|
+
if (typeof def.value === 'number') return 'number';
|
|
144
|
+
if (typeof def.value === 'boolean') return 'boolean';
|
|
145
|
+
}
|
|
146
|
+
if (def.type === 'ArrayExpression') return 'Array';
|
|
147
|
+
if (def.type === 'ObjectExpression') return 'Object';
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Check if function body has return statements with values
|
|
153
|
+
* @param {Object} node - Function AST node
|
|
154
|
+
* @returns {boolean}
|
|
155
|
+
*/
|
|
156
|
+
function hasReturnValue(node) {
|
|
157
|
+
let found = false;
|
|
158
|
+
try {
|
|
159
|
+
walk.simple(node.body, {
|
|
160
|
+
ReturnStatement(ret) {
|
|
161
|
+
if (ret.argument) found = true;
|
|
162
|
+
},
|
|
163
|
+
// Don't recurse into nested functions
|
|
164
|
+
FunctionDeclaration() { },
|
|
165
|
+
FunctionExpression() { },
|
|
166
|
+
ArrowFunctionExpression() { },
|
|
167
|
+
});
|
|
168
|
+
} catch (e) { /* walk error */ }
|
|
169
|
+
return found;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Validate a function's JSDoc against its AST
|
|
174
|
+
* @param {Object} jsdoc - Parsed JSDoc
|
|
175
|
+
* @param {Object[]} astParams - AST param nodes
|
|
176
|
+
* @param {Object} funcNode - AST function node
|
|
177
|
+
* @param {string} name - Function name
|
|
178
|
+
* @param {string} file - File path
|
|
179
|
+
* @param {number} line - Line number
|
|
180
|
+
* @returns {JSDocIssue[]}
|
|
181
|
+
*/
|
|
182
|
+
function validateFunction(jsdoc, astParams, funcNode, name, file, line) {
|
|
183
|
+
const issues = [];
|
|
184
|
+
|
|
185
|
+
if (!jsdoc) return issues; // No JSDoc = handled by undocumented checker
|
|
186
|
+
|
|
187
|
+
const docParams = jsdoc.params;
|
|
188
|
+
|
|
189
|
+
// 1. Param count mismatch
|
|
190
|
+
if (docParams.length !== astParams.length) {
|
|
191
|
+
issues.push({
|
|
192
|
+
file, line, name,
|
|
193
|
+
severity: 'error',
|
|
194
|
+
message: `Param count mismatch: JSDoc has ${docParams.length}, function has ${astParams.length}`,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 2. Param name mismatch
|
|
199
|
+
const minLen = Math.min(docParams.length, astParams.length);
|
|
200
|
+
for (let i = 0; i < minLen; i++) {
|
|
201
|
+
const docName = docParams[i].name;
|
|
202
|
+
const astName = extractParamName(astParams[i]);
|
|
203
|
+
|
|
204
|
+
if (docName !== astName && astName !== 'options' && astName !== 'args' && astName !== 'param') {
|
|
205
|
+
issues.push({
|
|
206
|
+
file, line, name,
|
|
207
|
+
severity: 'error',
|
|
208
|
+
message: `Param name mismatch at position ${i}: JSDoc says "${docName}", code has "${astName}"`,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 3. Missing @returns on non-void functions
|
|
214
|
+
if (!jsdoc.hasReturns && hasReturnValue(funcNode)) {
|
|
215
|
+
issues.push({
|
|
216
|
+
file, line, name,
|
|
217
|
+
severity: 'warning',
|
|
218
|
+
message: 'Function returns a value but JSDoc has no @returns',
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 4. Type hint inconsistency
|
|
223
|
+
for (let i = 0; i < minLen; i++) {
|
|
224
|
+
const docType = docParams[i].type;
|
|
225
|
+
const inferredType = inferTypeFromDefault(astParams[i]);
|
|
226
|
+
|
|
227
|
+
if (inferredType && docType && docType !== '*') {
|
|
228
|
+
let compatible = docType.includes(inferredType);
|
|
229
|
+
// Union types like 'a'|'b' are valid strings
|
|
230
|
+
if (!compatible && inferredType === 'string' && docType.includes("'") && docType.includes('|')) {
|
|
231
|
+
compatible = true;
|
|
232
|
+
}
|
|
233
|
+
// Type[] shorthand is a valid Array
|
|
234
|
+
if (!compatible && inferredType === 'Array' && docType.includes('[]')) {
|
|
235
|
+
compatible = true;
|
|
236
|
+
}
|
|
237
|
+
if (!compatible) {
|
|
238
|
+
issues.push({
|
|
239
|
+
file, line, name,
|
|
240
|
+
severity: 'warning',
|
|
241
|
+
message: `Type mismatch for "${docParams[i].name}": JSDoc says {${docType}}, default value suggests {${inferredType}}`,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return issues;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Check JSDoc consistency for a single file (per-file export for cache integration)
|
|
252
|
+
* @param {string} code
|
|
253
|
+
* @param {string} filePath
|
|
254
|
+
* @returns {JSDocIssue[]}
|
|
255
|
+
*/
|
|
256
|
+
export function checkJSDocFile(code, filePath) {
|
|
257
|
+
const issues = [];
|
|
258
|
+
|
|
259
|
+
let ast;
|
|
260
|
+
try {
|
|
261
|
+
ast = parse(code, { ecmaVersion: 'latest', sourceType: 'module', locations: true });
|
|
262
|
+
} catch (e) {
|
|
263
|
+
return issues;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const comments = extractJSDocComments(code);
|
|
267
|
+
|
|
268
|
+
walk.simple(ast, {
|
|
269
|
+
FunctionDeclaration(node) {
|
|
270
|
+
if (!node.id) return;
|
|
271
|
+
const jsdoc = findJSDocBefore(comments, node.loc.start.line);
|
|
272
|
+
if (jsdoc) {
|
|
273
|
+
issues.push(...validateFunction(jsdoc, node.params, node, node.id.name, filePath, node.loc.start.line));
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
// Exported arrow/const functions
|
|
278
|
+
VariableDeclaration(node) {
|
|
279
|
+
for (const decl of node.declarations) {
|
|
280
|
+
if (!decl.init) continue;
|
|
281
|
+
const func = decl.init.type === 'ArrowFunctionExpression' || decl.init.type === 'FunctionExpression'
|
|
282
|
+
? decl.init : null;
|
|
283
|
+
if (!func || !decl.id?.name) continue;
|
|
284
|
+
|
|
285
|
+
const jsdoc = findJSDocBefore(comments, node.loc.start.line);
|
|
286
|
+
if (jsdoc) {
|
|
287
|
+
issues.push(...validateFunction(jsdoc, func.params, func, decl.id.name, filePath, node.loc.start.line));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
ClassDeclaration(node) {
|
|
293
|
+
const className = node.id?.name || 'Anonymous';
|
|
294
|
+
for (const element of node.body.body) {
|
|
295
|
+
if (element.type !== 'MethodDefinition') continue;
|
|
296
|
+
const methodName = element.key.name || element.key.value;
|
|
297
|
+
if (!methodName || methodName === 'constructor') continue;
|
|
298
|
+
if (element.kind !== 'method') continue;
|
|
299
|
+
|
|
300
|
+
const funcNode = element.value;
|
|
301
|
+
const jsdoc = findJSDocBefore(comments, element.loc.start.line);
|
|
302
|
+
if (jsdoc) {
|
|
303
|
+
issues.push(...validateFunction(jsdoc, funcNode.params, funcNode, `${className}.${methodName}`, filePath, element.loc.start.line));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
return issues;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Check JSDoc consistency across a directory
|
|
314
|
+
* @param {string} dir
|
|
315
|
+
* @returns {{ issues: JSDocIssue[], summary: { total: number, errors: number, warnings: number, byFile: Object } }}
|
|
316
|
+
*/
|
|
317
|
+
export function checkJSDocConsistency(dir) {
|
|
318
|
+
const resolvedDir = resolve(dir);
|
|
319
|
+
const files = findJSFiles(dir);
|
|
320
|
+
const allIssues = [];
|
|
321
|
+
|
|
322
|
+
for (const file of files) {
|
|
323
|
+
let content;
|
|
324
|
+
try {
|
|
325
|
+
content = readFileSync(file, 'utf-8');
|
|
326
|
+
} catch (e) {
|
|
327
|
+
continue; // File deleted between findJSFiles and read
|
|
328
|
+
}
|
|
329
|
+
const relPath = relative(resolvedDir, file);
|
|
330
|
+
const issues = checkJSDocFile(content, relPath);
|
|
331
|
+
allIssues.push(...issues);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const errors = allIssues.filter(i => i.severity === 'error').length;
|
|
335
|
+
const warnings = allIssues.filter(i => i.severity === 'warning').length;
|
|
336
|
+
|
|
337
|
+
const byFile = {};
|
|
338
|
+
for (const issue of allIssues) {
|
|
339
|
+
byFile[issue.file] = (byFile[issue.file] || 0) + 1;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
issues: allIssues,
|
|
344
|
+
summary: {
|
|
345
|
+
total: allIssues.length,
|
|
346
|
+
errors,
|
|
347
|
+
warnings,
|
|
348
|
+
byFile,
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
}
|
package/src/jsdoc-generator.js
CHANGED
|
@@ -22,11 +22,9 @@ import { getWorkspaceRoot } from './workspace.js';
|
|
|
22
22
|
* Generate JSDoc for a single file
|
|
23
23
|
* @param {string} filePath - Absolute path to file
|
|
24
24
|
* @param {Object} [options]
|
|
25
|
-
* @param {boolean} [options.includeTests=true] - Include @test/@expect placeholders
|
|
26
25
|
* @returns {JSDocTemplate[]}
|
|
27
26
|
*/
|
|
28
27
|
export function generateJSDoc(filePath, options = {}) {
|
|
29
|
-
const includeTests = options.includeTests !== false;
|
|
30
28
|
const results = [];
|
|
31
29
|
|
|
32
30
|
const code = readFileSync(filePath, 'utf-8');
|
|
@@ -72,7 +70,6 @@ export function generateJSDoc(filePath, options = {}) {
|
|
|
72
70
|
name: node.id.name,
|
|
73
71
|
params: node.params,
|
|
74
72
|
async: node.async,
|
|
75
|
-
includeTests,
|
|
76
73
|
});
|
|
77
74
|
|
|
78
75
|
results.push({
|
|
@@ -102,7 +99,6 @@ export function generateJSDoc(filePath, options = {}) {
|
|
|
102
99
|
name: methodName,
|
|
103
100
|
params: funcNode.params,
|
|
104
101
|
async: funcNode.async,
|
|
105
|
-
includeTests,
|
|
106
102
|
});
|
|
107
103
|
|
|
108
104
|
results.push({
|
|
@@ -126,7 +122,6 @@ export function generateJSDoc(filePath, options = {}) {
|
|
|
126
122
|
* @param {string} info.name
|
|
127
123
|
* @param {Array} info.params
|
|
128
124
|
* @param {boolean} info.async
|
|
129
|
-
* @param {boolean} info.includeTests
|
|
130
125
|
* @returns {string}
|
|
131
126
|
*/
|
|
132
127
|
function buildJSDoc(info) {
|
|
@@ -145,12 +140,6 @@ function buildJSDoc(info) {
|
|
|
145
140
|
// Return type
|
|
146
141
|
lines.push(` * @returns {${info.async ? 'Promise<*>' : '*'}}`);
|
|
147
142
|
|
|
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
143
|
lines.push(' */');
|
|
155
144
|
return lines.join('\n');
|
|
156
145
|
}
|
package/src/large-files.js
CHANGED
|
@@ -54,6 +54,7 @@ function findJSFiles(dir, rootDir = dir) {
|
|
|
54
54
|
/**
|
|
55
55
|
* Analyze a single file
|
|
56
56
|
* @param {string} filePath
|
|
57
|
+
* @param {string} rootDir - Root directory for relative path calculation
|
|
57
58
|
* @returns {LargeFileItem}
|
|
58
59
|
*/
|
|
59
60
|
function analyzeFile(filePath, rootDir) {
|
package/src/mcp-server.js
CHANGED
|
@@ -21,11 +21,21 @@ import { getSimilarFunctions } from './similar-functions.js';
|
|
|
21
21
|
import { getComplexity } from './complexity.js';
|
|
22
22
|
import { getLargeFiles } from './large-files.js';
|
|
23
23
|
import { getOutdatedPatterns } from './outdated-patterns.js';
|
|
24
|
-
import { getFullAnalysis } from './full-analysis.js';
|
|
24
|
+
import { getFullAnalysis, getAnalysisSummaryOnly } from './full-analysis.js';
|
|
25
25
|
import { getCustomRules, setCustomRule, checkCustomRules } from './custom-rules.js';
|
|
26
26
|
import { getFrameworkReference } from './framework-references.js';
|
|
27
27
|
import { setRoots, resolvePath } from './workspace.js';
|
|
28
28
|
import { getDBSchema, getTableUsage, getDBDeadTables } from './db-analysis.js';
|
|
29
|
+
import { compressFile, editCompressed } from './compress.js';
|
|
30
|
+
import { getProjectDocs, generateContextFiles, checkStaleness } from './doc-dialect.js';
|
|
31
|
+
import { getGraph } from './tools.js';
|
|
32
|
+
import { parseProject, discoverSubProjects } from './parser.js';
|
|
33
|
+
import { getAiContext } from './ai-context.js';
|
|
34
|
+
import { checkJSDocConsistency } from './jsdoc-checker.js';
|
|
35
|
+
import { checkTypes } from './type-checker.js';
|
|
36
|
+
import { compactProject, expandProject } from './compact.js';
|
|
37
|
+
import { validateCtxContracts } from './ctx-to-jsdoc.js';
|
|
38
|
+
import { getConfig, setConfig, getModeDescription, getModeWorkflow } from './mode-config.js';
|
|
29
39
|
|
|
30
40
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
31
41
|
|
|
@@ -114,6 +124,105 @@ const TOOL_HANDLERS = {
|
|
|
114
124
|
get_db_schema: (args) => getDBSchema(resolvePath(args.path)),
|
|
115
125
|
get_table_usage: (args) => getTableUsage(resolvePath(args.path), args.table),
|
|
116
126
|
get_db_dead_tables: (args) => getDBDeadTables(resolvePath(args.path)),
|
|
127
|
+
|
|
128
|
+
// AI Context
|
|
129
|
+
get_compressed_file: (args) => compressFile(resolvePath(args.path), {
|
|
130
|
+
beautify: args.beautify,
|
|
131
|
+
legend: args.legend,
|
|
132
|
+
}),
|
|
133
|
+
get_project_docs: async (args) => {
|
|
134
|
+
const projectPath = resolvePath(args.path);
|
|
135
|
+
const graph = await getGraph(projectPath);
|
|
136
|
+
const docs = getProjectDocs(graph, projectPath, { file: args.file });
|
|
137
|
+
// Lazy staleness check — wrapped in try-catch for projects with parse errors
|
|
138
|
+
try {
|
|
139
|
+
const parsed = await parseProject(projectPath);
|
|
140
|
+
const staleness = checkStaleness(projectPath, parsed);
|
|
141
|
+
return { docs, staleFiles: staleness.stale, freshCount: staleness.fresh };
|
|
142
|
+
} catch { return { docs }; }
|
|
143
|
+
},
|
|
144
|
+
generate_context_docs: async (args) => {
|
|
145
|
+
const projectPath = resolvePath(args.path);
|
|
146
|
+
const graph = await getGraph(projectPath);
|
|
147
|
+
const parsed = await parseProject(projectPath);
|
|
148
|
+
return generateContextFiles(graph, projectPath, parsed, {
|
|
149
|
+
overwrite: args.overwrite,
|
|
150
|
+
scope: args.scope,
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
check_stale_docs: async (args) => {
|
|
154
|
+
const projectPath = resolvePath(args.path);
|
|
155
|
+
const parsed = await parseProject(projectPath);
|
|
156
|
+
return checkStaleness(projectPath, parsed);
|
|
157
|
+
},
|
|
158
|
+
get_ai_context: async (args) => {
|
|
159
|
+
const projectPath = resolvePath(args.path);
|
|
160
|
+
const result = await getAiContext(projectPath, {
|
|
161
|
+
includeFiles: args.includeFiles,
|
|
162
|
+
includeDocs: args.includeDocs,
|
|
163
|
+
includeSkeleton: args.includeSkeleton,
|
|
164
|
+
});
|
|
165
|
+
// Add staleness info
|
|
166
|
+
try {
|
|
167
|
+
const parsed = await parseProject(projectPath);
|
|
168
|
+
const staleness = checkStaleness(projectPath, parsed);
|
|
169
|
+
result.staleFiles = staleness.stale;
|
|
170
|
+
} catch { /* parse error — skip staleness */ }
|
|
171
|
+
return result;
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
// JSDoc Consistency
|
|
175
|
+
check_jsdoc_consistency: (args) => {
|
|
176
|
+
return checkJSDocConsistency(resolvePath(args.path));
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
// Type Checker (optional tsc)
|
|
180
|
+
check_types: async (args) => {
|
|
181
|
+
return checkTypes(resolvePath(args.path), {
|
|
182
|
+
files: args.files,
|
|
183
|
+
maxDiagnostics: args.maxDiagnostics,
|
|
184
|
+
});
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
// Monorepo & Performance
|
|
188
|
+
discover_sub_projects: (args) => {
|
|
189
|
+
return discoverSubProjects(resolvePath(args.path));
|
|
190
|
+
},
|
|
191
|
+
get_analysis_summary: (args) => {
|
|
192
|
+
return getAnalysisSummaryOnly(resolvePath(args.path));
|
|
193
|
+
},
|
|
194
|
+
compact_project: (args) => {
|
|
195
|
+
return compactProject(resolvePath(args.path), { dryRun: args.dryRun || false });
|
|
196
|
+
},
|
|
197
|
+
beautify_project: (args) => {
|
|
198
|
+
return expandProject(resolvePath(args.path), { dryRun: args.dryRun || false });
|
|
199
|
+
},
|
|
200
|
+
validate_ctx_contracts: (args) => {
|
|
201
|
+
return validateCtxContracts(resolvePath(args.path), { strict: args.strict || false });
|
|
202
|
+
},
|
|
203
|
+
edit_compressed: (args) => {
|
|
204
|
+
return editCompressed(resolvePath(args.path), args.symbol, args.code, {
|
|
205
|
+
beautify: args.beautify !== false,
|
|
206
|
+
dryRun: args.dryRun || false,
|
|
207
|
+
});
|
|
208
|
+
},
|
|
209
|
+
get_mode: (args) => {
|
|
210
|
+
const dir = resolvePath(args.path);
|
|
211
|
+
const config = getConfig(dir);
|
|
212
|
+
return {
|
|
213
|
+
...config,
|
|
214
|
+
description: getModeDescription(config.mode),
|
|
215
|
+
workflow: getModeWorkflow(config.mode),
|
|
216
|
+
};
|
|
217
|
+
},
|
|
218
|
+
set_mode: (args) => {
|
|
219
|
+
const dir = resolvePath(args.path);
|
|
220
|
+
const updates = { mode: args.mode };
|
|
221
|
+
if (args.beautify !== undefined) updates.beautify = args.beautify;
|
|
222
|
+
if (args.autoValidate !== undefined) updates.autoValidate = args.autoValidate;
|
|
223
|
+
if (args.stripJSDoc !== undefined) updates.stripJSDoc = args.stripJSDoc;
|
|
224
|
+
return setConfig(dir, updates);
|
|
225
|
+
},
|
|
117
226
|
};
|
|
118
227
|
|
|
119
228
|
/**
|
|
@@ -136,6 +245,10 @@ const RESPONSE_HINTS = {
|
|
|
136
245
|
hints.push('💡 Large class detected. Run get_complexity() to find refactoring targets.');
|
|
137
246
|
}
|
|
138
247
|
hints.push('💡 Use deps() to see what depends on this symbol.');
|
|
248
|
+
// Nudge: document if no .ctx exists
|
|
249
|
+
if (result.file) {
|
|
250
|
+
hints.push(`📝 No .ctx for ${result.file}? Run generate_context_docs({ scope: ["${result.file}"] }) to create documentation.`);
|
|
251
|
+
}
|
|
139
252
|
return hints;
|
|
140
253
|
},
|
|
141
254
|
|
|
@@ -205,6 +318,100 @@ const RESPONSE_HINTS = {
|
|
|
205
318
|
get_db_dead_tables: () => [
|
|
206
319
|
'💡 Dead columns detection is best-effort — verify before removing.',
|
|
207
320
|
],
|
|
321
|
+
|
|
322
|
+
get_compressed_file: (result) => {
|
|
323
|
+
const hints = [`💡 Saved ${result.savings} tokens (${result.original} → ${result.compressed}).`];
|
|
324
|
+
hints.push('💡 Use get_ai_context() for full project boot: skeleton + docs + compressed files.');
|
|
325
|
+
if (result.file) {
|
|
326
|
+
hints.push(`📝 Working on ${result.file}? Run generate_context_docs({ scope: ["${result.file}"] }) to document it.`);
|
|
327
|
+
}
|
|
328
|
+
return hints;
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
get_project_docs: (result) => {
|
|
332
|
+
const hints = [
|
|
333
|
+
'💡 Enrich docs by editing .context/*.ctx files — they are git-tracked.',
|
|
334
|
+
'💡 Use generate_context_docs() to create initial .ctx stubs.',
|
|
335
|
+
];
|
|
336
|
+
if (result.staleFiles?.length > 0) {
|
|
337
|
+
hints.push(`⚠️ ${result.staleFiles.length} .ctx files are STALE: ${result.staleFiles.slice(0, 5).join(', ')}. Run generate_context_docs({ scope: ${JSON.stringify(result.staleFiles)}, overwrite: true }) to update (descriptions will be preserved).`);
|
|
338
|
+
}
|
|
339
|
+
return hints;
|
|
340
|
+
},
|
|
341
|
+
|
|
342
|
+
check_stale_docs: (result) => {
|
|
343
|
+
const hints = [];
|
|
344
|
+
if (result.stale?.length > 0) {
|
|
345
|
+
hints.push(`⚠️ ${result.stale.length} stale: ${result.stale.join(', ')}`);
|
|
346
|
+
hints.push(`💡 Run generate_context_docs({ scope: ${JSON.stringify(result.stale)}, overwrite: true }) — existing descriptions will be preserved.`);
|
|
347
|
+
} else {
|
|
348
|
+
hints.push('✅ All .ctx docs are up to date.');
|
|
349
|
+
}
|
|
350
|
+
if (result.unknown > 0) {
|
|
351
|
+
hints.push(`ℹ️ ${result.unknown} .ctx files without @sig header (pre-staleness format).`);
|
|
352
|
+
}
|
|
353
|
+
return hints;
|
|
354
|
+
},
|
|
355
|
+
|
|
356
|
+
generate_context_docs: (result) => {
|
|
357
|
+
const hints = [];
|
|
358
|
+
if (result.created?.length > 0) {
|
|
359
|
+
hints.push(`✅ Created ${result.created.length} .ctx files with @sig hashes.`);
|
|
360
|
+
}
|
|
361
|
+
if (result.skipped?.length > 0) {
|
|
362
|
+
hints.push(`ℹ️ Skipped ${result.skipped.length} existing files. Use overwrite=true to regenerate (descriptions are preserved via merge).`);
|
|
363
|
+
}
|
|
364
|
+
if (result.templates && Object.keys(result.templates).length > 0) {
|
|
365
|
+
hints.push(`📝 .ctx files have {DESCRIBE} markers. To enrich automatically:`);
|
|
366
|
+
hints.push(` delegate_task({ prompt: "Enrich .context/*.ctx files — replace {DESCRIBE} with compact descriptions", skill: "doc-enricher" })`);
|
|
367
|
+
hints.push(` Or enrich manually: read source files and replace {DESCRIBE} markers with pipe-separated descriptions (max 80 chars).`);
|
|
368
|
+
}
|
|
369
|
+
return hints;
|
|
370
|
+
},
|
|
371
|
+
|
|
372
|
+
get_ai_context: (result) => {
|
|
373
|
+
const hints = [`💡 Context loaded: ${result.totalTokens} tokens (${result.savings} savings vs ${result.vsOriginal} original).`];
|
|
374
|
+
hints.push('💡 Use expand() to drill into specific symbols. Use get_compressed_file() for additional files.');
|
|
375
|
+
if (result.staleFiles?.length > 0) {
|
|
376
|
+
hints.push(`⚠️ ${result.staleFiles.length} .ctx docs are stale. Run generate_context_docs({ scope: ${JSON.stringify(result.staleFiles)}, overwrite: true }) then delegate_task({ skill: "doc-enricher" }) to update.`);
|
|
377
|
+
}
|
|
378
|
+
return hints;
|
|
379
|
+
},
|
|
380
|
+
|
|
381
|
+
validate_ctx_contracts: (result) => {
|
|
382
|
+
const hints = [];
|
|
383
|
+
if (result.summary?.errors > 0) {
|
|
384
|
+
hints.push(`⚠️ ${result.summary.errors} contract violations found. Run generate_context_docs({ overwrite: true }) to regenerate .ctx files.`);
|
|
385
|
+
} else {
|
|
386
|
+
hints.push('✅ All .ctx contracts valid — documentation matches source.');
|
|
387
|
+
}
|
|
388
|
+
return hints;
|
|
389
|
+
},
|
|
390
|
+
|
|
391
|
+
edit_compressed: (result) => {
|
|
392
|
+
const hints = [];
|
|
393
|
+
if (result.success) {
|
|
394
|
+
hints.push(`✅ Symbol "${result.symbol}" replaced in ${result.file}.`);
|
|
395
|
+
hints.push('💡 Run invalidate_cache() to refresh the graph after editing.');
|
|
396
|
+
hints.push('💡 Run validate_ctx_contracts() to check if .ctx docs need updating.');
|
|
397
|
+
}
|
|
398
|
+
return hints;
|
|
399
|
+
},
|
|
400
|
+
|
|
401
|
+
get_mode: (result) => {
|
|
402
|
+
const hints = [`📋 Current mode: ${result.mode} — ${result.description}`];
|
|
403
|
+
if (result.mode === 2) {
|
|
404
|
+
hints.push('💡 Workflow: get_compressed_file() → read → edit_compressed() → write.');
|
|
405
|
+
}
|
|
406
|
+
return hints;
|
|
407
|
+
},
|
|
408
|
+
|
|
409
|
+
set_mode: (result) => {
|
|
410
|
+
if (result.saved) {
|
|
411
|
+
return [`✅ Mode set to ${result.config.mode}. Saved to ${result.path}.`];
|
|
412
|
+
}
|
|
413
|
+
return [];
|
|
414
|
+
},
|
|
208
415
|
};
|
|
209
416
|
|
|
210
417
|
/**
|