project-graph-mcp 1.5.0 → 2.1.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 +171 -31
- package/docs/img/explorer-compact.jpg +0 -0
- package/docs/img/explorer-expanded.jpg +0 -0
- package/package.json +12 -8
- package/src/.project-graph-cache.json +1 -1
- 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/.project-graph-cache.json +1 -0
- package/src/compact/ai-context.js +7 -0
- package/src/compact/compact-migrate.js +17 -0
- package/src/compact/compact.js +18 -0
- package/src/compact/compress.js +14 -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/web/.project-graph-cache.json +1 -0
- package/web/app.js +17 -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/ai-context.js +0 -113
- package/src/analysis-cache.js +0 -155
- package/src/cli-handlers.js +0 -271
- package/src/cli.js +0 -95
- package/src/compact.js +0 -207
- package/src/complexity.js +0 -237
- package/src/compress.js +0 -319
- package/src/ctx-to-jsdoc.js +0 -514
- package/src/custom-rules.js +0 -584
- package/src/db-analysis.js +0 -194
- package/src/dead-code.js +0 -468
- package/src/doc-dialect.js +0 -716
- package/src/filters.js +0 -227
- package/src/framework-references.js +0 -177
- package/src/full-analysis.js +0 -470
- package/src/graph-builder.js +0 -299
- package/src/instructions.js +0 -73
- package/src/jsdoc-checker.js +0 -351
- package/src/jsdoc-generator.js +0 -203
- 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 -163
- package/src/mcp-server.js +0 -675
- package/src/mode-config.js +0 -127
- package/src/outdated-patterns.js +0 -296
- package/src/parser.js +0 -662
- package/src/server.js +0 -28
- package/src/similar-functions.js +0 -279
- package/src/test-annotations.js +0 -323
- package/src/tool-defs.js +0 -793
- package/src/tools.js +0 -470
- package/src/type-checker.js +0 -188
- package/src/undocumented.js +0 -259
- package/src/workspace.js +0 -70
- /package/{AGENT_ROLE.md → docs/examples/AGENT_ROLE.md} +0 -0
- /package/{AGENT_ROLE_MINIMAL.md → docs/examples/AGENT_ROLE_MINIMAL.md} +0 -0
package/src/similar-functions.js
DELETED
|
@@ -1,279 +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
|
-
* @param {string} rootDir - Root directory for relative path calculation
|
|
67
|
-
* @returns {FunctionSignature[]}
|
|
68
|
-
*/
|
|
69
|
-
function extractSignatures(filePath, rootDir) {
|
|
70
|
-
const code = readFileSync(filePath, 'utf-8');
|
|
71
|
-
const relPath = relative(rootDir, filePath);
|
|
72
|
-
const signatures = [];
|
|
73
|
-
|
|
74
|
-
let ast;
|
|
75
|
-
try {
|
|
76
|
-
ast = parse(code, { ecmaVersion: 'latest', sourceType: 'module', locations: true });
|
|
77
|
-
} catch (e) {
|
|
78
|
-
return signatures;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
walk.simple(ast, {
|
|
82
|
-
FunctionDeclaration(node) {
|
|
83
|
-
if (!node.id) return;
|
|
84
|
-
signatures.push(buildSignature(node, node.id.name, relPath));
|
|
85
|
-
},
|
|
86
|
-
|
|
87
|
-
MethodDefinition(node) {
|
|
88
|
-
if (node.kind !== 'method') return;
|
|
89
|
-
const name = node.key.name || node.key.value;
|
|
90
|
-
if (name.startsWith('_')) return;
|
|
91
|
-
signatures.push(buildSignature(node.value, name, relPath));
|
|
92
|
-
},
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
return signatures;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Build signature from function node
|
|
100
|
-
* @param {Object} node
|
|
101
|
-
* @param {string} name
|
|
102
|
-
* @param {string} file
|
|
103
|
-
* @returns {FunctionSignature}
|
|
104
|
-
*/
|
|
105
|
-
function buildSignature(node, name, file) {
|
|
106
|
-
const paramNames = node.params.map(p => extractParamName(p));
|
|
107
|
-
const calls = [];
|
|
108
|
-
|
|
109
|
-
// Extract function calls
|
|
110
|
-
walk.simple(node.body, {
|
|
111
|
-
CallExpression(callNode) {
|
|
112
|
-
if (callNode.callee.type === 'Identifier') {
|
|
113
|
-
calls.push(callNode.callee.name);
|
|
114
|
-
} else if (callNode.callee.type === 'MemberExpression' && callNode.callee.property.type === 'Identifier') {
|
|
115
|
-
calls.push(callNode.callee.property.name);
|
|
116
|
-
}
|
|
117
|
-
},
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
// Create structural hash
|
|
121
|
-
const bodyHash = hashBodyStructure(node.body);
|
|
122
|
-
|
|
123
|
-
return {
|
|
124
|
-
name,
|
|
125
|
-
file,
|
|
126
|
-
line: node.loc?.start?.line || 0,
|
|
127
|
-
paramCount: node.params.length,
|
|
128
|
-
paramNames,
|
|
129
|
-
async: node.async || false,
|
|
130
|
-
bodyHash,
|
|
131
|
-
calls: [...new Set(calls)],
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Extract param name
|
|
137
|
-
* @param {Object} param
|
|
138
|
-
* @returns {string}
|
|
139
|
-
*/
|
|
140
|
-
function extractParamName(param) {
|
|
141
|
-
if (param.type === 'Identifier') return param.name;
|
|
142
|
-
if (param.type === 'AssignmentPattern' && param.left.type === 'Identifier') return param.left.name;
|
|
143
|
-
if (param.type === 'RestElement' && param.argument.type === 'Identifier') return param.argument.name;
|
|
144
|
-
return 'param';
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Create structural hash of function body
|
|
149
|
-
* @param {Object} body
|
|
150
|
-
* @returns {string}
|
|
151
|
-
*/
|
|
152
|
-
function hashBodyStructure(body) {
|
|
153
|
-
const structure = [];
|
|
154
|
-
|
|
155
|
-
walk.simple(body, {
|
|
156
|
-
IfStatement() { structure.push('IF'); },
|
|
157
|
-
ForStatement() { structure.push('FOR'); },
|
|
158
|
-
ForOfStatement() { structure.push('FOROF'); },
|
|
159
|
-
ForInStatement() { structure.push('FORIN'); },
|
|
160
|
-
WhileStatement() { structure.push('WHILE'); },
|
|
161
|
-
SwitchStatement() { structure.push('SWITCH'); },
|
|
162
|
-
TryStatement() { structure.push('TRY'); },
|
|
163
|
-
ReturnStatement() { structure.push('RET'); },
|
|
164
|
-
ThrowStatement() { structure.push('THROW'); },
|
|
165
|
-
AwaitExpression() { structure.push('AWAIT'); },
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
return structure.join('|');
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Calculate similarity between two functions
|
|
173
|
-
* @param {FunctionSignature} a
|
|
174
|
-
* @param {FunctionSignature} b
|
|
175
|
-
* @returns {{similarity: number, reasons: string[]}}
|
|
176
|
-
*/
|
|
177
|
-
function calculateSimilarity(a, b) {
|
|
178
|
-
const reasons = [];
|
|
179
|
-
let score = 0;
|
|
180
|
-
let maxScore = 0;
|
|
181
|
-
|
|
182
|
-
// Same param count (important)
|
|
183
|
-
maxScore += 30;
|
|
184
|
-
if (a.paramCount === b.paramCount) {
|
|
185
|
-
score += 30;
|
|
186
|
-
reasons.push('Same param count');
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Similar param names
|
|
190
|
-
maxScore += 20;
|
|
191
|
-
const commonParams = a.paramNames.filter(p => b.paramNames.includes(p));
|
|
192
|
-
if (commonParams.length > 0 && a.paramNames.length > 0) {
|
|
193
|
-
const paramSim = commonParams.length / Math.max(a.paramNames.length, b.paramNames.length);
|
|
194
|
-
score += Math.round(paramSim * 20);
|
|
195
|
-
if (paramSim >= 0.5) reasons.push(`Similar params: ${commonParams.join(', ')}`);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Same async status
|
|
199
|
-
maxScore += 10;
|
|
200
|
-
if (a.async === b.async) {
|
|
201
|
-
score += 10;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Similar body structure
|
|
205
|
-
maxScore += 25;
|
|
206
|
-
if (a.bodyHash === b.bodyHash && a.bodyHash.length > 0) {
|
|
207
|
-
score += 25;
|
|
208
|
-
reasons.push('Identical structure');
|
|
209
|
-
} else if (a.bodyHash.length > 0 && b.bodyHash.length > 0) {
|
|
210
|
-
const aTokens = a.bodyHash.split('|');
|
|
211
|
-
const bTokens = b.bodyHash.split('|');
|
|
212
|
-
const commonTokens = aTokens.filter(t => bTokens.includes(t));
|
|
213
|
-
if (commonTokens.length > 0) {
|
|
214
|
-
const structSim = commonTokens.length / Math.max(aTokens.length, bTokens.length);
|
|
215
|
-
score += Math.round(structSim * 25);
|
|
216
|
-
if (structSim >= 0.5) reasons.push('Similar control flow');
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Common function calls
|
|
221
|
-
maxScore += 15;
|
|
222
|
-
const commonCalls = a.calls.filter(c => b.calls.includes(c));
|
|
223
|
-
if (commonCalls.length > 0 && a.calls.length > 0 && b.calls.length > 0) {
|
|
224
|
-
const callSim = commonCalls.length / Math.max(a.calls.length, b.calls.length);
|
|
225
|
-
score += Math.round(callSim * 15);
|
|
226
|
-
if (commonCalls.length >= 2) reasons.push(`Common calls: ${commonCalls.slice(0, 3).join(', ')}`);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const similarity = Math.round((score / maxScore) * 100);
|
|
230
|
-
return { similarity, reasons };
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Get similar functions in directory
|
|
235
|
-
* @param {string} dir
|
|
236
|
-
* @param {Object} [options]
|
|
237
|
-
* @param {number} [options.threshold=60] - Minimum similarity percentage
|
|
238
|
-
* @returns {Promise<{total: number, pairs: SimilarPair[]}>}
|
|
239
|
-
*/
|
|
240
|
-
export async function getSimilarFunctions(dir, options = {}) {
|
|
241
|
-
const threshold = options.threshold || 60;
|
|
242
|
-
const resolvedDir = resolve(dir);
|
|
243
|
-
const files = findJSFiles(dir);
|
|
244
|
-
const allSignatures = [];
|
|
245
|
-
|
|
246
|
-
// Collect all signatures
|
|
247
|
-
for (const file of files) {
|
|
248
|
-
allSignatures.push(...extractSignatures(file, resolvedDir));
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Compare all pairs
|
|
252
|
-
const pairs = [];
|
|
253
|
-
for (let i = 0; i < allSignatures.length; i++) {
|
|
254
|
-
for (let j = i + 1; j < allSignatures.length; j++) {
|
|
255
|
-
const a = allSignatures[i];
|
|
256
|
-
const b = allSignatures[j];
|
|
257
|
-
|
|
258
|
-
// Skip same file same name (likely intentional overload)
|
|
259
|
-
if (a.file === b.file && a.name === b.name) continue;
|
|
260
|
-
|
|
261
|
-
// Skip very small functions
|
|
262
|
-
if (a.bodyHash.length < 3 && b.bodyHash.length < 3) continue;
|
|
263
|
-
|
|
264
|
-
const { similarity, reasons } = calculateSimilarity(a, b);
|
|
265
|
-
|
|
266
|
-
if (similarity >= threshold && reasons.length > 0) {
|
|
267
|
-
pairs.push({ a, b, similarity, reasons });
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// Sort by similarity descending
|
|
273
|
-
pairs.sort((x, y) => y.similarity - x.similarity);
|
|
274
|
-
|
|
275
|
-
return {
|
|
276
|
-
total: pairs.length,
|
|
277
|
-
pairs: pairs.slice(0, 20),
|
|
278
|
-
};
|
|
279
|
-
}
|
package/src/test-annotations.js
DELETED
|
@@ -1,323 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Test Checklists — .ctx.md based
|
|
3
|
-
* Reads/writes test checklists from ## Tests sections in .ctx.md files
|
|
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.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
|
-
*/
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* @typedef {Object} Feature
|
|
20
|
-
* @property {string} name - Function/method name
|
|
21
|
-
* @property {TestStep[]} tests - Test steps
|
|
22
|
-
* @property {string} file - Source .ctx.md file path
|
|
23
|
-
*/
|
|
24
|
-
|
|
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
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Parse ## Tests section from a .ctx.md file
|
|
50
|
-
* @param {string} content - File content
|
|
51
|
-
* @param {string} filePath - Path to .ctx.md file
|
|
52
|
-
* @returns {Feature[]}
|
|
53
|
-
*/
|
|
54
|
-
export function parseAnnotations(content, filePath) {
|
|
55
|
-
const lines = content.split('\n');
|
|
56
|
-
const features = [];
|
|
57
|
-
|
|
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;
|
|
72
|
-
}
|
|
73
|
-
|
|
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();
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
currentTests.push({ name, action, expected, status, failReason });
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Flush remaining
|
|
100
|
-
if (inTests && currentTests.length) {
|
|
101
|
-
features.push(...groupByName(currentTests, filePath));
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return features;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
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[]}
|
|
112
|
-
*/
|
|
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;
|
|
121
|
-
}
|
|
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
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return Object.entries(map).map(([name, tests]) => ({
|
|
132
|
-
name,
|
|
133
|
-
tests,
|
|
134
|
-
file: filePath,
|
|
135
|
-
}));
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Get all features from a project directory
|
|
140
|
-
* @param {string} dir - Project root
|
|
141
|
-
* @returns {Feature[]}
|
|
142
|
-
*/
|
|
143
|
-
export function getAllFeatures(dir) {
|
|
144
|
-
const contextDir = join(resolve(dir), '.context');
|
|
145
|
-
const files = findCtxMdFiles(contextDir);
|
|
146
|
-
const features = [];
|
|
147
|
-
|
|
148
|
-
for (const file of files) {
|
|
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
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return features;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Get pending (uncompleted) tests
|
|
163
|
-
* @param {string} dir - Project root
|
|
164
|
-
* @returns {TestStep[]}
|
|
165
|
-
*/
|
|
166
|
-
export function getPendingTests(dir) {
|
|
167
|
-
const resolvedDir = resolve(dir);
|
|
168
|
-
const features = getAllFeatures(dir);
|
|
169
|
-
const pending = [];
|
|
170
|
-
|
|
171
|
-
for (const feature of features) {
|
|
172
|
-
for (const test of feature.tests) {
|
|
173
|
-
if (test.status === 'pending') {
|
|
174
|
-
pending.push({
|
|
175
|
-
...test,
|
|
176
|
-
feature: feature.name,
|
|
177
|
-
file: relative(resolvedDir, feature.file),
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return pending;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
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}}
|
|
190
|
-
*/
|
|
191
|
-
export function markTestPassed(testId) {
|
|
192
|
-
const name = testId.split('.')[0];
|
|
193
|
-
return updateTestState(name, testId, 'x');
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
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}}
|
|
201
|
-
*/
|
|
202
|
-
export function markTestFailed(testId, reason) {
|
|
203
|
-
const name = testId.split('.')[0];
|
|
204
|
-
return updateTestState(name, testId, '!', reason);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
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
|
|
262
|
-
* @returns {Object}
|
|
263
|
-
*/
|
|
264
|
-
export function getTestSummary(dir) {
|
|
265
|
-
const features = getAllFeatures(dir);
|
|
266
|
-
|
|
267
|
-
let total = 0;
|
|
268
|
-
let passed = 0;
|
|
269
|
-
let failed = 0;
|
|
270
|
-
let pending = 0;
|
|
271
|
-
const failures = [];
|
|
272
|
-
|
|
273
|
-
for (const feature of features) {
|
|
274
|
-
for (const test of feature.tests) {
|
|
275
|
-
total++;
|
|
276
|
-
if (test.status === 'passed') {
|
|
277
|
-
passed++;
|
|
278
|
-
} else if (test.status === 'failed') {
|
|
279
|
-
failed++;
|
|
280
|
-
failures.push({ id: test.id, reason: test.failReason });
|
|
281
|
-
} else {
|
|
282
|
-
pending++;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
return {
|
|
288
|
-
total,
|
|
289
|
-
passed,
|
|
290
|
-
failed,
|
|
291
|
-
pending,
|
|
292
|
-
progress: total > 0 ? Math.round((passed + failed) / total * 100) : 0,
|
|
293
|
-
failures,
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Reset all test states — changes [x] and [!] back to [ ] in all .ctx.md files
|
|
299
|
-
* @returns {{success: boolean}}
|
|
300
|
-
*/
|
|
301
|
-
export function resetTestState() {
|
|
302
|
-
const cwd = process.cwd();
|
|
303
|
-
const contextDir = join(cwd, '.context');
|
|
304
|
-
const files = findCtxMdFiles(contextDir);
|
|
305
|
-
|
|
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');
|
|
316
|
-
}
|
|
317
|
-
} catch (e) {
|
|
318
|
-
// Skip
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
return { success: true };
|
|
323
|
-
}
|