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.
Files changed (125) hide show
  1. package/README.md +171 -31
  2. package/docs/img/explorer-compact.jpg +0 -0
  3. package/docs/img/explorer-expanded.jpg +0 -0
  4. package/package.json +12 -8
  5. package/src/.project-graph-cache.json +1 -1
  6. package/src/analysis/analysis-cache.js +7 -0
  7. package/src/analysis/complexity.js +14 -0
  8. package/src/analysis/custom-rules.js +36 -0
  9. package/src/analysis/db-analysis.js +9 -0
  10. package/src/analysis/dead-code.js +19 -0
  11. package/src/analysis/full-analysis.js +18 -0
  12. package/src/analysis/jsdoc-checker.js +24 -0
  13. package/src/analysis/jsdoc-generator.js +10 -0
  14. package/src/analysis/large-files.js +11 -0
  15. package/src/analysis/outdated-patterns.js +12 -0
  16. package/src/analysis/similar-functions.js +16 -0
  17. package/src/analysis/test-annotations.js +21 -0
  18. package/src/analysis/type-checker.js +8 -0
  19. package/src/analysis/undocumented.js +14 -0
  20. package/src/cli/cli-handlers.js +4 -0
  21. package/src/cli/cli.js +5 -0
  22. package/src/compact/.project-graph-cache.json +1 -0
  23. package/src/compact/ai-context.js +7 -0
  24. package/src/compact/compact-migrate.js +17 -0
  25. package/src/compact/compact.js +18 -0
  26. package/src/compact/compress.js +14 -0
  27. package/src/compact/ctx-to-jsdoc.js +29 -0
  28. package/src/compact/doc-dialect.js +30 -0
  29. package/src/compact/expand.js +37 -0
  30. package/src/compact/framework-references.js +5 -0
  31. package/src/compact/instructions.js +3 -0
  32. package/src/compact/mode-config.js +8 -0
  33. package/src/compact/validate-pipeline.js +9 -0
  34. package/src/core/event-bus.js +9 -0
  35. package/src/core/filters.js +14 -0
  36. package/src/core/graph-builder.js +12 -0
  37. package/src/core/parser.js +31 -0
  38. package/src/core/workspace.js +8 -0
  39. package/src/lang/lang-go.js +17 -0
  40. package/src/lang/lang-python.js +12 -0
  41. package/src/lang/lang-sql.js +23 -0
  42. package/src/lang/lang-typescript.js +9 -0
  43. package/src/lang/lang-utils.js +4 -0
  44. package/src/mcp/mcp-server.js +17 -0
  45. package/src/mcp/tool-defs.js +3 -0
  46. package/src/mcp/tools.js +25 -0
  47. package/src/network/backend-lifecycle.js +19 -0
  48. package/src/network/backend.js +5 -0
  49. package/src/network/local-gateway.js +23 -0
  50. package/src/network/mdns.js +13 -0
  51. package/src/network/server.js +10 -0
  52. package/src/network/web-server.js +34 -0
  53. package/web/.project-graph-cache.json +1 -0
  54. package/web/app.js +17 -0
  55. package/web/components/code-block.js +3 -0
  56. package/web/components/quick-open.js +5 -0
  57. package/web/dashboard-state.js +3 -0
  58. package/web/dashboard.html +27 -0
  59. package/web/dashboard.js +8 -0
  60. package/web/highlight.js +13 -0
  61. package/web/index.html +35 -0
  62. package/web/panels/ActionBoard/ActionBoard.css.js +1 -0
  63. package/web/panels/ActionBoard/ActionBoard.js +4 -0
  64. package/web/panels/ActionBoard/ActionBoard.tpl.js +1 -0
  65. package/web/panels/EventItem/EventItem.css.js +1 -0
  66. package/web/panels/EventItem/EventItem.js +4 -0
  67. package/web/panels/EventItem/EventItem.tpl.js +1 -0
  68. package/web/panels/ProjectItem/ProjectItem.css.js +1 -0
  69. package/web/panels/ProjectItem/ProjectItem.js +5 -0
  70. package/web/panels/ProjectItem/ProjectItem.tpl.js +1 -0
  71. package/web/panels/ProjectList/ProjectList.css.js +1 -0
  72. package/web/panels/ProjectList/ProjectList.js +4 -0
  73. package/web/panels/ProjectList/ProjectList.tpl.js +1 -0
  74. package/web/panels/SettingsPanel/.project-graph-cache.json +1 -0
  75. package/web/panels/SettingsPanel/SettingsPanel.css.js +1 -0
  76. package/web/panels/SettingsPanel/SettingsPanel.js +7 -0
  77. package/web/panels/SettingsPanel/SettingsPanel.tpl.js +1 -0
  78. package/web/panels/code-viewer.js +5 -0
  79. package/web/panels/ctx-panel.js +4 -0
  80. package/web/panels/dep-graph.js +6 -0
  81. package/web/panels/file-tree.js +188 -0
  82. package/web/panels/health-panel.js +3 -0
  83. package/web/panels/live-monitor.js +3 -0
  84. package/web/state.js +17 -0
  85. package/web/style.css +157 -0
  86. package/references/symbiote-3x.md +0 -834
  87. package/src/ai-context.js +0 -113
  88. package/src/analysis-cache.js +0 -155
  89. package/src/cli-handlers.js +0 -271
  90. package/src/cli.js +0 -95
  91. package/src/compact.js +0 -207
  92. package/src/complexity.js +0 -237
  93. package/src/compress.js +0 -319
  94. package/src/ctx-to-jsdoc.js +0 -514
  95. package/src/custom-rules.js +0 -584
  96. package/src/db-analysis.js +0 -194
  97. package/src/dead-code.js +0 -468
  98. package/src/doc-dialect.js +0 -716
  99. package/src/filters.js +0 -227
  100. package/src/framework-references.js +0 -177
  101. package/src/full-analysis.js +0 -470
  102. package/src/graph-builder.js +0 -299
  103. package/src/instructions.js +0 -73
  104. package/src/jsdoc-checker.js +0 -351
  105. package/src/jsdoc-generator.js +0 -203
  106. package/src/lang-go.js +0 -285
  107. package/src/lang-python.js +0 -197
  108. package/src/lang-sql.js +0 -309
  109. package/src/lang-typescript.js +0 -190
  110. package/src/lang-utils.js +0 -124
  111. package/src/large-files.js +0 -163
  112. package/src/mcp-server.js +0 -675
  113. package/src/mode-config.js +0 -127
  114. package/src/outdated-patterns.js +0 -296
  115. package/src/parser.js +0 -662
  116. package/src/server.js +0 -28
  117. package/src/similar-functions.js +0 -279
  118. package/src/test-annotations.js +0 -323
  119. package/src/tool-defs.js +0 -793
  120. package/src/tools.js +0 -470
  121. package/src/type-checker.js +0 -188
  122. package/src/undocumented.js +0 -259
  123. package/src/workspace.js +0 -70
  124. /package/{AGENT_ROLE.md → docs/examples/AGENT_ROLE.md} +0 -0
  125. /package/{AGENT_ROLE_MINIMAL.md → docs/examples/AGENT_ROLE_MINIMAL.md} +0 -0
@@ -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
- }
@@ -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
- }