project-graph-mcp 1.2.4 → 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 +105 -29
- package/AGENT_ROLE_MINIMAL.md +23 -8
- package/README.md +100 -14
- package/package.json +1 -1
- package/src/.project-graph-cache.json +1 -1
- 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/db-analysis.js +194 -0
- package/src/doc-dialect.js +716 -0
- package/src/full-analysis.js +322 -11
- package/src/graph-builder.js +32 -2
- package/src/instructions.js +3 -105
- package/src/jsdoc-checker.js +351 -0
- package/src/jsdoc-generator.js +0 -11
- package/src/lang-sql.js +309 -0
- package/src/large-files.js +1 -0
- package/src/mcp-server.js +236 -1
- package/src/mode-config.js +127 -0
- package/src/outdated-patterns.js +1 -0
- package/src/parser.js +364 -34
- package/src/similar-functions.js +1 -0
- package/src/test-annotations.js +203 -181
- package/src/tool-defs.js +318 -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
package/src/test-annotations.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Test
|
|
3
|
-
*
|
|
2
|
+
* Test Checklists — .ctx.md based
|
|
3
|
+
* Reads/writes test checklists from ## Tests sections in .ctx.md files
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { readFileSync, readdirSync, statSync, writeFileSync } from 'fs';
|
|
@@ -8,143 +8,151 @@ import { join, basename, relative, resolve } from 'path';
|
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* @typedef {Object} TestStep
|
|
11
|
-
* @property {string} id - Unique ID (e.g., "togglePin.
|
|
12
|
-
* @property {string}
|
|
13
|
-
* @property {string}
|
|
14
|
-
* @property {
|
|
15
|
-
* @property {string} [failReason] - Why it failed
|
|
11
|
+
* @property {string} id - Unique ID (e.g., "togglePin.0")
|
|
12
|
+
* @property {string} action - What to do
|
|
13
|
+
* @property {string} [expected] - Expected result (after →)
|
|
14
|
+
* @property {string} status - 'pending' | 'passed' | 'failed'
|
|
15
|
+
* @property {string} [failReason] - Why it failed
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* @typedef {Object} Feature
|
|
20
|
-
* @property {string} name -
|
|
21
|
-
* @property {string} description - What the method does
|
|
20
|
+
* @property {string} name - Function/method name
|
|
22
21
|
* @property {TestStep[]} tests - Test steps
|
|
23
|
-
* @property {
|
|
24
|
-
* @property {string} file - Source file
|
|
25
|
-
* @property {number} line - Line number
|
|
22
|
+
* @property {string} file - Source .ctx.md file path
|
|
26
23
|
*/
|
|
27
24
|
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Find all .ctx.md files in .context/ directory
|
|
27
|
+
* @param {string} dir - Context directory path
|
|
28
|
+
* @returns {string[]}
|
|
29
|
+
*/
|
|
30
|
+
function findCtxMdFiles(dir) {
|
|
31
|
+
const files = [];
|
|
32
|
+
try {
|
|
33
|
+
for (const entry of readdirSync(dir)) {
|
|
34
|
+
const fullPath = join(dir, entry);
|
|
35
|
+
const stat = statSync(fullPath);
|
|
36
|
+
if (stat.isDirectory() && !entry.startsWith('.')) {
|
|
37
|
+
files.push(...findCtxMdFiles(fullPath));
|
|
38
|
+
} else if (entry.endsWith('.ctx.md')) {
|
|
39
|
+
files.push(fullPath);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
} catch (e) {
|
|
43
|
+
// Directory not found
|
|
44
|
+
}
|
|
45
|
+
return files;
|
|
46
|
+
}
|
|
30
47
|
|
|
31
48
|
/**
|
|
32
|
-
* Parse
|
|
33
|
-
* @param {string} content
|
|
34
|
-
* @param {string} filePath
|
|
49
|
+
* Parse ## Tests section from a .ctx.md file
|
|
50
|
+
* @param {string} content - File content
|
|
51
|
+
* @param {string} filePath - Path to .ctx.md file
|
|
35
52
|
* @returns {Feature[]}
|
|
36
53
|
*/
|
|
37
54
|
export function parseAnnotations(content, filePath) {
|
|
38
|
-
const
|
|
39
|
-
const
|
|
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
|
-
}
|
|
55
|
+
const lines = content.split('\n');
|
|
56
|
+
const features = [];
|
|
80
57
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
58
|
+
// Find ## Tests section
|
|
59
|
+
let inTests = false;
|
|
60
|
+
let currentTests = [];
|
|
61
|
+
|
|
62
|
+
for (const line of lines) {
|
|
63
|
+
// Detect section headers
|
|
64
|
+
if (line.startsWith('## ')) {
|
|
65
|
+
if (inTests && currentTests.length) {
|
|
66
|
+
// End of Tests section — flush
|
|
67
|
+
features.push(...groupByName(currentTests, filePath));
|
|
68
|
+
currentTests = [];
|
|
69
|
+
}
|
|
70
|
+
inTests = line.startsWith('## Tests');
|
|
71
|
+
continue;
|
|
90
72
|
}
|
|
91
73
|
|
|
92
|
-
if (
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
74
|
+
if (!inTests) continue;
|
|
75
|
+
|
|
76
|
+
// Parse checklist lines: - [ ] name: action → expected
|
|
77
|
+
// States: [ ] = pending, [x] = passed, [!] = failed
|
|
78
|
+
const match = line.match(/^- \[([ x!])\] (\w+):\s*(.+)$/);
|
|
79
|
+
if (!match) continue;
|
|
80
|
+
|
|
81
|
+
const [, state, name, rest] = match;
|
|
82
|
+
const parts = rest.split('→').map(s => s.trim());
|
|
83
|
+
const action = parts[0];
|
|
84
|
+
const expected = parts[1] || null;
|
|
85
|
+
|
|
86
|
+
// Extract fail reason from: (FAILED: reason)
|
|
87
|
+
let failReason = null;
|
|
88
|
+
let status = 'pending';
|
|
89
|
+
if (state === 'x') status = 'passed';
|
|
90
|
+
if (state === '!') {
|
|
91
|
+
status = 'failed';
|
|
92
|
+
const failMatch = action.match(/\(FAILED:\s*(.+)\)$/);
|
|
93
|
+
if (failMatch) failReason = failMatch[1].trim();
|
|
103
94
|
}
|
|
95
|
+
|
|
96
|
+
currentTests.push({ name, action, expected, status, failReason });
|
|
104
97
|
}
|
|
105
98
|
|
|
106
|
-
|
|
99
|
+
// Flush remaining
|
|
100
|
+
if (inTests && currentTests.length) {
|
|
101
|
+
features.push(...groupByName(currentTests, filePath));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return features;
|
|
107
105
|
}
|
|
108
106
|
|
|
109
107
|
/**
|
|
110
|
-
*
|
|
111
|
-
* @param {
|
|
112
|
-
* @
|
|
108
|
+
* Group test steps by function name into Feature objects
|
|
109
|
+
* @param {Array} tests - Raw test entries
|
|
110
|
+
* @param {string} filePath - Source file
|
|
111
|
+
* @returns {Feature[]}
|
|
113
112
|
*/
|
|
114
|
-
function
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
}
|
|
113
|
+
function groupByName(tests, filePath) {
|
|
114
|
+
const map = {};
|
|
115
|
+
let indexMap = {};
|
|
116
|
+
|
|
117
|
+
for (const t of tests) {
|
|
118
|
+
if (!map[t.name]) {
|
|
119
|
+
map[t.name] = [];
|
|
120
|
+
indexMap[t.name] = 0;
|
|
127
121
|
}
|
|
128
|
-
|
|
129
|
-
|
|
122
|
+
map[t.name].push({
|
|
123
|
+
id: `${t.name}.${indexMap[t.name]++}`,
|
|
124
|
+
action: t.action,
|
|
125
|
+
expected: t.expected,
|
|
126
|
+
status: t.status,
|
|
127
|
+
failReason: t.failReason,
|
|
128
|
+
});
|
|
130
129
|
}
|
|
131
130
|
|
|
132
|
-
return
|
|
131
|
+
return Object.entries(map).map(([name, tests]) => ({
|
|
132
|
+
name,
|
|
133
|
+
tests,
|
|
134
|
+
file: filePath,
|
|
135
|
+
}));
|
|
133
136
|
}
|
|
134
137
|
|
|
135
138
|
/**
|
|
136
|
-
* Get all features from a directory
|
|
137
|
-
* @param {string} dir
|
|
139
|
+
* Get all features from a project directory
|
|
140
|
+
* @param {string} dir - Project root
|
|
138
141
|
* @returns {Feature[]}
|
|
139
142
|
*/
|
|
140
143
|
export function getAllFeatures(dir) {
|
|
141
|
-
const
|
|
144
|
+
const contextDir = join(resolve(dir), '.context');
|
|
145
|
+
const files = findCtxMdFiles(contextDir);
|
|
142
146
|
const features = [];
|
|
143
147
|
|
|
144
148
|
for (const file of files) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
149
|
+
try {
|
|
150
|
+
const content = readFileSync(file, 'utf-8');
|
|
151
|
+
const parsed = parseAnnotations(content, file);
|
|
152
|
+
features.push(...parsed);
|
|
153
|
+
} catch (e) {
|
|
154
|
+
// Skip unreadable files
|
|
155
|
+
}
|
|
148
156
|
}
|
|
149
157
|
|
|
150
158
|
return features;
|
|
@@ -152,7 +160,7 @@ export function getAllFeatures(dir) {
|
|
|
152
160
|
|
|
153
161
|
/**
|
|
154
162
|
* Get pending (uncompleted) tests
|
|
155
|
-
* @param {string} dir
|
|
163
|
+
* @param {string} dir - Project root
|
|
156
164
|
* @returns {TestStep[]}
|
|
157
165
|
*/
|
|
158
166
|
export function getPendingTests(dir) {
|
|
@@ -162,8 +170,7 @@ export function getPendingTests(dir) {
|
|
|
162
170
|
|
|
163
171
|
for (const feature of features) {
|
|
164
172
|
for (const test of feature.tests) {
|
|
165
|
-
|
|
166
|
-
if (!state || !state.completed) {
|
|
173
|
+
if (test.status === 'pending') {
|
|
167
174
|
pending.push({
|
|
168
175
|
...test,
|
|
169
176
|
feature: feature.name,
|
|
@@ -177,27 +184,81 @@ export function getPendingTests(dir) {
|
|
|
177
184
|
}
|
|
178
185
|
|
|
179
186
|
/**
|
|
180
|
-
* Mark a test as passed
|
|
181
|
-
* @param {string} testId
|
|
187
|
+
* Mark a test as passed — writes directly to .ctx.md file
|
|
188
|
+
* @param {string} testId - e.g. "togglePin.0"
|
|
189
|
+
* @returns {{success: boolean, testId: string}}
|
|
182
190
|
*/
|
|
183
191
|
export function markTestPassed(testId) {
|
|
184
|
-
|
|
185
|
-
return
|
|
192
|
+
const name = testId.split('.')[0];
|
|
193
|
+
return updateTestState(name, testId, 'x');
|
|
186
194
|
}
|
|
187
195
|
|
|
188
196
|
/**
|
|
189
|
-
* Mark a test as failed
|
|
190
|
-
* @param {string} testId
|
|
191
|
-
* @param {string} reason
|
|
197
|
+
* Mark a test as failed — writes directly to .ctx.md file
|
|
198
|
+
* @param {string} testId - e.g. "togglePin.0"
|
|
199
|
+
* @param {string} reason - Why it failed
|
|
200
|
+
* @returns {{success: boolean, testId: string, reason: string}}
|
|
192
201
|
*/
|
|
193
202
|
export function markTestFailed(testId, reason) {
|
|
194
|
-
|
|
195
|
-
return
|
|
203
|
+
const name = testId.split('.')[0];
|
|
204
|
+
return updateTestState(name, testId, '!', reason);
|
|
196
205
|
}
|
|
197
206
|
|
|
198
207
|
/**
|
|
199
|
-
*
|
|
200
|
-
* @param {string}
|
|
208
|
+
* Update test state in .ctx.md files
|
|
209
|
+
* @param {string} name - Function name
|
|
210
|
+
* @param {string} testId - Full test ID
|
|
211
|
+
* @param {string} newState - 'x' | '!' | ' '
|
|
212
|
+
* @param {string} [reason] - Failure reason
|
|
213
|
+
* @returns {{success: boolean, testId: string, reason?: string}}
|
|
214
|
+
*/
|
|
215
|
+
function updateTestState(name, testId, newState, reason) {
|
|
216
|
+
// Need to find which .ctx.md file contains this test
|
|
217
|
+
// Walk all .ctx.md files in .context/
|
|
218
|
+
const cwd = process.cwd();
|
|
219
|
+
const contextDir = join(cwd, '.context');
|
|
220
|
+
const files = findCtxMdFiles(contextDir);
|
|
221
|
+
const testIndex = parseInt(testId.split('.')[1], 10);
|
|
222
|
+
|
|
223
|
+
for (const file of files) {
|
|
224
|
+
try {
|
|
225
|
+
const content = readFileSync(file, 'utf-8');
|
|
226
|
+
const lines = content.split('\n');
|
|
227
|
+
let inTests = false;
|
|
228
|
+
let nameIndex = 0;
|
|
229
|
+
|
|
230
|
+
for (let i = 0; i < lines.length; i++) {
|
|
231
|
+
if (lines[i].startsWith('## ')) {
|
|
232
|
+
inTests = lines[i].startsWith('## Tests');
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (!inTests) continue;
|
|
236
|
+
|
|
237
|
+
const match = lines[i].match(/^- \[([ x!])\] (\w+):\s*(.+)$/);
|
|
238
|
+
if (!match) continue;
|
|
239
|
+
if (match[2] !== name) continue;
|
|
240
|
+
|
|
241
|
+
if (nameIndex === testIndex) {
|
|
242
|
+
// Found the line — update it
|
|
243
|
+
const desc = match[3].replace(/\s*\(FAILED:.*\)$/, '');
|
|
244
|
+
const suffix = reason ? ` (FAILED: ${reason})` : '';
|
|
245
|
+
lines[i] = `- [${newState}] ${name}: ${desc}${suffix}`;
|
|
246
|
+
writeFileSync(file, lines.join('\n'), 'utf-8');
|
|
247
|
+
return { success: true, testId, ...(reason ? { reason } : {}) };
|
|
248
|
+
}
|
|
249
|
+
nameIndex++;
|
|
250
|
+
}
|
|
251
|
+
} catch (e) {
|
|
252
|
+
// Skip
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return { success: false, testId, error: 'Test not found' };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Get test summary across all .ctx.md files
|
|
261
|
+
* @param {string} dir - Project root
|
|
201
262
|
* @returns {Object}
|
|
202
263
|
*/
|
|
203
264
|
export function getTestSummary(dir) {
|
|
@@ -212,18 +273,13 @@ export function getTestSummary(dir) {
|
|
|
212
273
|
for (const feature of features) {
|
|
213
274
|
for (const test of feature.tests) {
|
|
214
275
|
total++;
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (!state || !state.completed) {
|
|
218
|
-
pending++;
|
|
219
|
-
} else if (state.passed) {
|
|
276
|
+
if (test.status === 'passed') {
|
|
220
277
|
passed++;
|
|
221
|
-
} else {
|
|
278
|
+
} else if (test.status === 'failed') {
|
|
222
279
|
failed++;
|
|
223
|
-
failures.push({
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
});
|
|
280
|
+
failures.push({ id: test.id, reason: test.failReason });
|
|
281
|
+
} else {
|
|
282
|
+
pending++;
|
|
227
283
|
}
|
|
228
284
|
}
|
|
229
285
|
}
|
|
@@ -239,63 +295,29 @@ export function getTestSummary(dir) {
|
|
|
239
295
|
}
|
|
240
296
|
|
|
241
297
|
/**
|
|
242
|
-
* Reset test
|
|
298
|
+
* Reset all test states — changes [x] and [!] back to [ ] in all .ctx.md files
|
|
299
|
+
* @returns {{success: boolean}}
|
|
243
300
|
*/
|
|
244
301
|
export function resetTestState() {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
302
|
+
const cwd = process.cwd();
|
|
303
|
+
const contextDir = join(cwd, '.context');
|
|
304
|
+
const files = findCtxMdFiles(contextDir);
|
|
248
305
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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('');
|
|
306
|
+
for (const file of files) {
|
|
307
|
+
try {
|
|
308
|
+
let content = readFileSync(file, 'utf-8');
|
|
309
|
+
// Replace [x] and [!] with [ ] in test lines, remove FAILED reasons
|
|
310
|
+
const updated = content.replace(
|
|
311
|
+
/^(- )\[([x!])\] (\w+:\s*.+?)(?:\s*\(FAILED:.*\))?$/gm,
|
|
312
|
+
'$1[ ] $3'
|
|
313
|
+
);
|
|
314
|
+
if (updated !== content) {
|
|
315
|
+
writeFileSync(file, updated, 'utf-8');
|
|
296
316
|
}
|
|
317
|
+
} catch (e) {
|
|
318
|
+
// Skip
|
|
297
319
|
}
|
|
298
320
|
}
|
|
299
321
|
|
|
300
|
-
return
|
|
322
|
+
return { success: true };
|
|
301
323
|
}
|