purecontext-mcp 1.1.8 → 1.2.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 (167) hide show
  1. package/AGENT_INSTRUCTIONS.md +393 -0
  2. package/AGENT_INSTRUCTIONS_SHORT.md +53 -0
  3. package/AST-SEARCH.md +274 -0
  4. package/CHANGELOG.md +62 -0
  5. package/CODE-INTELLIGENCE.md +369 -0
  6. package/HEALTH-DASHBOARDS.md +241 -0
  7. package/README.md +7 -0
  8. package/REFACTORING-SAFELY.md +279 -0
  9. package/UNDERSTANDING-RELATIONSHIPS.md +240 -0
  10. package/USER-GUIDE.md +14 -0
  11. package/VISUALIZING-CODE.md +199 -0
  12. package/WORKFLOW-TECH-DEBT.md +286 -0
  13. package/dist/core/db/dep-store.d.ts +75 -0
  14. package/dist/core/db/dep-store.d.ts.map +1 -1
  15. package/dist/core/db/dep-store.js +277 -0
  16. package/dist/core/db/dep-store.js.map +1 -1
  17. package/dist/core/db/schema.d.ts.map +1 -1
  18. package/dist/core/db/schema.js +12 -0
  19. package/dist/core/db/schema.js.map +1 -1
  20. package/dist/core/index-manager.js +1 -1
  21. package/dist/core/index-manager.js.map +1 -1
  22. package/dist/core/types.d.ts +5 -0
  23. package/dist/core/types.d.ts.map +1 -1
  24. package/dist/graph/diagram-renderer.d.ts +83 -0
  25. package/dist/graph/diagram-renderer.d.ts.map +1 -0
  26. package/dist/graph/diagram-renderer.js +294 -0
  27. package/dist/graph/diagram-renderer.js.map +1 -0
  28. package/dist/graph/graph-traversal.d.ts +92 -0
  29. package/dist/graph/graph-traversal.d.ts.map +1 -1
  30. package/dist/graph/graph-traversal.js +440 -2
  31. package/dist/graph/graph-traversal.js.map +1 -1
  32. package/dist/server/mcp-server.d.ts.map +1 -1
  33. package/dist/server/mcp-server.js +145 -0
  34. package/dist/server/mcp-server.js.map +1 -1
  35. package/dist/server/tools/check-delete-safe.d.ts +50 -0
  36. package/dist/server/tools/check-delete-safe.d.ts.map +1 -0
  37. package/dist/server/tools/check-delete-safe.js +308 -0
  38. package/dist/server/tools/check-delete-safe.js.map +1 -0
  39. package/dist/server/tools/check-move-safe.d.ts +44 -0
  40. package/dist/server/tools/check-move-safe.d.ts.map +1 -0
  41. package/dist/server/tools/check-move-safe.js +266 -0
  42. package/dist/server/tools/check-move-safe.js.map +1 -0
  43. package/dist/server/tools/check-rename-safe.d.ts +48 -0
  44. package/dist/server/tools/check-rename-safe.d.ts.map +1 -0
  45. package/dist/server/tools/check-rename-safe.js +218 -0
  46. package/dist/server/tools/check-rename-safe.js.map +1 -0
  47. package/dist/server/tools/diff-health-radar.d.ts +44 -0
  48. package/dist/server/tools/diff-health-radar.d.ts.map +1 -0
  49. package/dist/server/tools/diff-health-radar.js +192 -0
  50. package/dist/server/tools/diff-health-radar.js.map +1 -0
  51. package/dist/server/tools/find-cycles.d.ts +31 -0
  52. package/dist/server/tools/find-cycles.d.ts.map +1 -0
  53. package/dist/server/tools/find-cycles.js +85 -0
  54. package/dist/server/tools/find-cycles.js.map +1 -0
  55. package/dist/server/tools/find-implementations.d.ts +47 -0
  56. package/dist/server/tools/find-implementations.d.ts.map +1 -0
  57. package/dist/server/tools/find-implementations.js +167 -0
  58. package/dist/server/tools/find-implementations.js.map +1 -0
  59. package/dist/server/tools/find-untested-symbols.d.ts +52 -0
  60. package/dist/server/tools/find-untested-symbols.d.ts.map +1 -0
  61. package/dist/server/tools/find-untested-symbols.js +308 -0
  62. package/dist/server/tools/find-untested-symbols.js.map +1 -0
  63. package/dist/server/tools/get-architecture-snapshot.d.ts +43 -0
  64. package/dist/server/tools/get-architecture-snapshot.d.ts.map +1 -0
  65. package/dist/server/tools/get-architecture-snapshot.js +292 -0
  66. package/dist/server/tools/get-architecture-snapshot.js.map +1 -0
  67. package/dist/server/tools/get-call-hierarchy.d.ts +43 -0
  68. package/dist/server/tools/get-call-hierarchy.d.ts.map +1 -0
  69. package/dist/server/tools/get-call-hierarchy.js +119 -0
  70. package/dist/server/tools/get-call-hierarchy.js.map +1 -0
  71. package/dist/server/tools/get-class-hierarchy.d.ts +36 -0
  72. package/dist/server/tools/get-class-hierarchy.d.ts.map +1 -0
  73. package/dist/server/tools/get-class-hierarchy.js +125 -0
  74. package/dist/server/tools/get-class-hierarchy.js.map +1 -0
  75. package/dist/server/tools/get-complexity-hotspots.d.ts +50 -0
  76. package/dist/server/tools/get-complexity-hotspots.d.ts.map +1 -0
  77. package/dist/server/tools/get-complexity-hotspots.js +282 -0
  78. package/dist/server/tools/get-complexity-hotspots.js.map +1 -0
  79. package/dist/server/tools/get-coupling-map.d.ts +39 -0
  80. package/dist/server/tools/get-coupling-map.d.ts.map +1 -0
  81. package/dist/server/tools/get-coupling-map.js +107 -0
  82. package/dist/server/tools/get-coupling-map.js.map +1 -0
  83. package/dist/server/tools/get-debt-report.d.ts +44 -0
  84. package/dist/server/tools/get-debt-report.d.ts.map +1 -0
  85. package/dist/server/tools/get-debt-report.js +606 -0
  86. package/dist/server/tools/get-debt-report.js.map +1 -0
  87. package/dist/server/tools/get-entry-points.d.ts +79 -0
  88. package/dist/server/tools/get-entry-points.d.ts.map +1 -0
  89. package/dist/server/tools/get-entry-points.js +362 -0
  90. package/dist/server/tools/get-entry-points.js.map +1 -0
  91. package/dist/server/tools/get-public-api.d.ts +53 -0
  92. package/dist/server/tools/get-public-api.d.ts.map +1 -0
  93. package/dist/server/tools/get-public-api.js +218 -0
  94. package/dist/server/tools/get-public-api.js.map +1 -0
  95. package/dist/server/tools/get-test-coverage-map.d.ts +66 -0
  96. package/dist/server/tools/get-test-coverage-map.d.ts.map +1 -0
  97. package/dist/server/tools/get-test-coverage-map.js +588 -0
  98. package/dist/server/tools/get-test-coverage-map.js.map +1 -0
  99. package/dist/server/tools/get-todos.d.ts +51 -0
  100. package/dist/server/tools/get-todos.d.ts.map +1 -0
  101. package/dist/server/tools/get-todos.js +180 -0
  102. package/dist/server/tools/get-todos.js.map +1 -0
  103. package/dist/server/tools/get-type-graph.d.ts +73 -0
  104. package/dist/server/tools/get-type-graph.d.ts.map +1 -0
  105. package/dist/server/tools/get-type-graph.js +437 -0
  106. package/dist/server/tools/get-type-graph.js.map +1 -0
  107. package/dist/server/tools/health-radar.d.ts +50 -0
  108. package/dist/server/tools/health-radar.d.ts.map +1 -0
  109. package/dist/server/tools/health-radar.js +426 -0
  110. package/dist/server/tools/health-radar.js.map +1 -0
  111. package/dist/server/tools/plan-refactoring.d.ts +74 -0
  112. package/dist/server/tools/plan-refactoring.d.ts.map +1 -0
  113. package/dist/server/tools/plan-refactoring.js +644 -0
  114. package/dist/server/tools/plan-refactoring.js.map +1 -0
  115. package/dist/server/tools/render-call-graph.d.ts +40 -0
  116. package/dist/server/tools/render-call-graph.d.ts.map +1 -0
  117. package/dist/server/tools/render-call-graph.js +215 -0
  118. package/dist/server/tools/render-call-graph.js.map +1 -0
  119. package/dist/server/tools/render-class-hierarchy.d.ts +42 -0
  120. package/dist/server/tools/render-class-hierarchy.d.ts.map +1 -0
  121. package/dist/server/tools/render-class-hierarchy.js +265 -0
  122. package/dist/server/tools/render-class-hierarchy.js.map +1 -0
  123. package/dist/server/tools/render-dep-matrix.d.ts +38 -0
  124. package/dist/server/tools/render-dep-matrix.d.ts.map +1 -0
  125. package/dist/server/tools/render-dep-matrix.js +186 -0
  126. package/dist/server/tools/render-dep-matrix.js.map +1 -0
  127. package/dist/server/tools/render-diagram.d.ts +47 -0
  128. package/dist/server/tools/render-diagram.d.ts.map +1 -0
  129. package/dist/server/tools/render-diagram.js +266 -0
  130. package/dist/server/tools/render-diagram.js.map +1 -0
  131. package/dist/server/tools/render-import-graph.d.ts +41 -0
  132. package/dist/server/tools/render-import-graph.d.ts.map +1 -0
  133. package/dist/server/tools/render-import-graph.js +158 -0
  134. package/dist/server/tools/render-import-graph.js.map +1 -0
  135. package/dist/server/tools/search-ast.d.ts +55 -0
  136. package/dist/server/tools/search-ast.d.ts.map +1 -0
  137. package/dist/server/tools/search-ast.js +279 -0
  138. package/dist/server/tools/search-ast.js.map +1 -0
  139. package/dist/server/tools/search-by-complexity.d.ts +92 -0
  140. package/dist/server/tools/search-by-complexity.d.ts.map +1 -0
  141. package/dist/server/tools/search-by-complexity.js +268 -0
  142. package/dist/server/tools/search-by-complexity.js.map +1 -0
  143. package/dist/server/tools/search-by-decorator.d.ts +48 -0
  144. package/dist/server/tools/search-by-decorator.d.ts.map +1 -0
  145. package/dist/server/tools/search-by-decorator.js +518 -0
  146. package/dist/server/tools/search-by-decorator.js.map +1 -0
  147. package/dist/server/tools/search-by-signature.d.ts +56 -0
  148. package/dist/server/tools/search-by-signature.d.ts.map +1 -0
  149. package/dist/server/tools/search-by-signature.js +200 -0
  150. package/dist/server/tools/search-by-signature.js.map +1 -0
  151. package/dist/ui/assets/BlastRadius-QdgESOL8.js +1 -0
  152. package/dist/ui/assets/{DependencyGraph-B60SMPbX.js → DependencyGraph-BSMhzwWV.js} +1 -1
  153. package/dist/ui/assets/{NotFound-Bsg8Wppk.js → NotFound-CipFP_s1.js} +1 -1
  154. package/dist/ui/assets/{RepoDetail-Cie0D4_s.js → RepoDetail-Dfp5z5Kq.js} +1 -1
  155. package/dist/ui/assets/{RepoList-CldNt89M.js → RepoList-BKtST3hB.js} +1 -1
  156. package/dist/ui/assets/{Search-CgvpHMOi.js → Search-DzhGDViy.js} +1 -1
  157. package/dist/ui/assets/{SymbolView-B9h0LZTz.js → SymbolView-ryVEwAHG.js} +1 -1
  158. package/dist/ui/assets/{index-DADf5y_L.css → index-Ny8gn9F0.css} +1 -1
  159. package/dist/ui/assets/{index-eK0wMkUR.js → index-nX2chMqi.js} +2 -2
  160. package/dist/ui/assets/{useSearch-KAl3Qyi2.js → useSearch-BnBCRKui.js} +1 -1
  161. package/dist/ui/index.html +2 -2
  162. package/dist/version.d.ts +1 -1
  163. package/dist/version.js +1 -1
  164. package/docs/dev/jcodemunch-gap-analysis.md +198 -0
  165. package/package.json +9 -1
  166. package/user-manual.md +2466 -0
  167. package/dist/ui/assets/BlastRadius-RP7vJEyQ.js +0 -1
@@ -0,0 +1,308 @@
1
+ /**
2
+ * find-untested-symbols.ts
3
+ *
4
+ * MCP tool: find_untested_symbols
5
+ *
6
+ * Identifies exported symbols (functions, methods, classes, etc.) in the
7
+ * codebase that do not appear to have any test coverage — i.e. whose names
8
+ * are not referenced in any test file.
9
+ *
10
+ * Detection strategy (static heuristic):
11
+ * 1. Identify test files by path convention:
12
+ * - Path segment is "test", "tests", "spec", "specs", or "__tests__"
13
+ * - Filename ends with .test.*, .spec.*, _test.*, or _spec.*
14
+ * 2. Extract all identifiers referenced in those test files.
15
+ * 3. Any non-test symbol whose name does NOT appear in the test identifier
16
+ * set is considered "untested".
17
+ *
18
+ * Limitations:
19
+ * - Name-based matching only; dynamic dispatch, aliases, and indirect calls
20
+ * can produce false negatives (symbol appears tested when it is not) or
21
+ * false positives (common names like `get` match unrelated things).
22
+ * - Does not read coverage reports; for accurate coverage, pipe in a
23
+ * coverage JSON file via get_test_coverage_map instead.
24
+ *
25
+ * Priority rating (per untested symbol):
26
+ * high — cyclomatic_complexity >= 5 OR line_count >= 20
27
+ * medium — cyclomatic_complexity >= 2 OR line_count >= 8
28
+ * low — all other symbols
29
+ */
30
+ import { z } from 'zod';
31
+ import { openDatabase, getRepo } from '../../core/db/schema.js';
32
+ import { buildMeta } from './_meta.js';
33
+ export const name = 'find_untested_symbols';
34
+ export const description = 'Identify symbols (functions, methods, classes) that do not appear to have ' +
35
+ 'test coverage by scanning test files for references to each symbol name. ' +
36
+ 'Returns a prioritised list of untested symbols ranked by cyclomatic ' +
37
+ 'complexity so you can focus testing effort where it matters most.' +
38
+ '\n\nDetection strategy: static heuristic — symbol name must appear as an ' +
39
+ 'identifier in at least one test file (path-based detection). ' +
40
+ 'This is not a coverage-report parser; use get_test_coverage_map for ' +
41
+ 'line-level accuracy.' +
42
+ '\n\nDiffers from related tools:' +
43
+ '\n find_dead_code — exports with no importers (unused API)' +
44
+ '\n get_test_coverage_map — line-level coverage from a coverage JSON report' +
45
+ '\n get_complexity_hotspots — complexity ranking without test-coverage filter';
46
+ // ─── Default testable kinds ────────────────────────────────────────────────────
47
+ const DEFAULT_KINDS = ['function', 'method', 'class', 'interface', 'middleware', 'route'];
48
+ // ─── Input schema ──────────────────────────────────────────────────────────────
49
+ export const inputSchema = {
50
+ repoId: z.string().describe('Repository ID returned by index_folder or list_repos'),
51
+ scope: z
52
+ .string()
53
+ .optional()
54
+ .describe('Restrict analysis to a directory prefix (e.g. "src/services/"). ' +
55
+ 'Omit to analyse the whole repo.'),
56
+ kinds: z
57
+ .array(z.string())
58
+ .optional()
59
+ .describe(`Symbol kinds to include (default: ${DEFAULT_KINDS.join(', ')}). ` +
60
+ 'Narrow to e.g. ["function","method"] to focus on callable units.'),
61
+ filePath: z
62
+ .string()
63
+ .optional()
64
+ .describe('Restrict to a specific source file path (relative to repo root).'),
65
+ minLineCount: z
66
+ .number().int().min(1)
67
+ .optional()
68
+ .describe('Minimum symbol line count to include (default 3). ' +
69
+ 'Filters out trivial one-liners that are not worth testing individually.'),
70
+ includeTestRefs: z
71
+ .boolean()
72
+ .optional()
73
+ .describe('When true, include the list of test files that reference each ' +
74
+ 'symbol (the "testRefs" field). Increases response size. Default false.'),
75
+ limit: z
76
+ .number().int().min(1).max(500)
77
+ .optional()
78
+ .describe('Maximum number of untested symbols to return (default 50).'),
79
+ };
80
+ // ─── Helpers ───────────────────────────────────────────────────────────────────
81
+ /**
82
+ * Returns true when a file path looks like a test file by convention.
83
+ */
84
+ function isTestFile(filePath) {
85
+ // Normalise separators
86
+ const norm = filePath.replace(/\\/g, '/');
87
+ // Path-segment match: /test/, /tests/, /spec/, /specs/, /__tests__/
88
+ if (/(?:^|\/)(?:tests?|specs?|__tests__)\//.test(norm))
89
+ return true;
90
+ // Filename suffix: .test.ts, .spec.ts, .test.js, _test.go, etc.
91
+ if (/[._](?:test|spec)\.[a-z]+$/.test(norm))
92
+ return true;
93
+ // Filename prefix: test_foo.py, spec_foo.rb
94
+ const filename = norm.split('/').pop() ?? '';
95
+ if (/^(?:test|spec)_/.test(filename))
96
+ return true;
97
+ return false;
98
+ }
99
+ /**
100
+ * Extract all identifier tokens from source text.
101
+ * Returns a Set for O(1) membership checks.
102
+ */
103
+ function extractIdentifiers(text) {
104
+ const ids = new Set();
105
+ for (const m of text.matchAll(/\b([A-Za-z_$][A-Za-z0-9_$]*)\b/g)) {
106
+ ids.add(m[1]);
107
+ }
108
+ return ids;
109
+ }
110
+ /**
111
+ * Assign a priority to an untested symbol based on its complexity and size.
112
+ */
113
+ function computePriority(cc, lineCount) {
114
+ if (cc >= 5 || lineCount >= 20)
115
+ return 'high';
116
+ if (cc >= 2 || lineCount >= 8)
117
+ return 'medium';
118
+ return 'low';
119
+ }
120
+ /**
121
+ * Compute the 1-based line number for a given byte offset in a buffer.
122
+ */
123
+ function lineOf(buf, startByte) {
124
+ const slice = buf.slice(0, Math.min(startByte, buf.length));
125
+ let line = 1;
126
+ for (let i = 0; i < slice.length; i++) {
127
+ if (slice[i] === 0x0a)
128
+ line++;
129
+ }
130
+ return line;
131
+ }
132
+ // ─── Handler ───────────────────────────────────────────────────────────────────
133
+ export async function handler(args) {
134
+ const t0 = Date.now();
135
+ const { repoId, minLineCount = 3, includeTestRefs = false, limit = 50, } = args;
136
+ const kinds = (args.kinds && args.kinds.length > 0)
137
+ ? args.kinds
138
+ : DEFAULT_KINDS;
139
+ const db = openDatabase(repoId);
140
+ try {
141
+ // ── Validate repo ─────────────────────────────────────────────────────────
142
+ const repo = getRepo(db, repoId);
143
+ if (!repo) {
144
+ return {
145
+ content: [{
146
+ type: 'text',
147
+ text: JSON.stringify({ error: `Repo "${repoId}" not found. Run index_folder first.` }),
148
+ }],
149
+ isError: true,
150
+ };
151
+ }
152
+ // ── Load all indexed files (with content) ─────────────────────────────────
153
+ const allFiles = db
154
+ .prepare('SELECT path, raw_content FROM files WHERE repo_id = ?')
155
+ .all(repoId);
156
+ // ── Partition files: test vs source ───────────────────────────────────────
157
+ const testFiles = [];
158
+ for (const f of allFiles) {
159
+ if (isTestFile(f.path))
160
+ testFiles.push(f);
161
+ }
162
+ // ── Build identifier sets per test file ───────────────────────────────────
163
+ // identifiersByTestFile[i] = Set of identifiers in testFiles[i]
164
+ const identifiersByTestFile = [];
165
+ // Global union for fast "is this symbol name referenced at all?" check
166
+ const allTestIdentifiers = new Set();
167
+ for (const tf of testFiles) {
168
+ if (!tf.raw_content)
169
+ continue;
170
+ const text = tf.raw_content.toString('utf8');
171
+ const ids = extractIdentifiers(text);
172
+ identifiersByTestFile.push({ path: tf.path, ids });
173
+ for (const id of ids)
174
+ allTestIdentifiers.add(id);
175
+ }
176
+ // ── Load candidate symbols ────────────────────────────────────────────────
177
+ const kindPlaceholders = kinds.map(() => '?').join(', ');
178
+ const conditions = [
179
+ 'repo_id = ?',
180
+ `kind IN (${kindPlaceholders})`,
181
+ 'COALESCE(line_count, 1) >= ?',
182
+ ];
183
+ const params = [repoId, ...kinds, minLineCount];
184
+ if (args.scope) {
185
+ const prefix = args.scope.endsWith('/') ? args.scope : `${args.scope}/`;
186
+ conditions.push('(file_path = ? OR file_path LIKE ?)');
187
+ params.push(args.scope, `${prefix}%`);
188
+ }
189
+ if (args.filePath) {
190
+ conditions.push('file_path = ?');
191
+ params.push(args.filePath);
192
+ }
193
+ const sql = `
194
+ SELECT id, name, kind, file_path, start_byte, signature, summary,
195
+ COALESCE(line_count, 1) AS line_count,
196
+ COALESCE(cyclomatic_complexity, 1) AS cyclomatic_complexity
197
+ FROM symbols
198
+ WHERE ${conditions.join(' AND ')}
199
+ AND file_path NOT IN (SELECT path FROM files WHERE repo_id = ? AND path IN (
200
+ ${allFiles.filter(f => isTestFile(f.path)).map(() => '?').join(',') || 'SELECT NULL'}
201
+ ))
202
+ ORDER BY cyclomatic_complexity DESC, line_count DESC
203
+ `;
204
+ // Avoid building a huge IN clause for test-file exclusion — instead
205
+ // filter in JS after the query (simpler and still fast for typical repos).
206
+ const sqlSimple = `
207
+ SELECT id, name, kind, file_path, start_byte, signature, summary,
208
+ COALESCE(line_count, 1) AS line_count,
209
+ COALESCE(cyclomatic_complexity, 1) AS cyclomatic_complexity
210
+ FROM symbols
211
+ WHERE ${conditions.join(' AND ')}
212
+ ORDER BY cyclomatic_complexity DESC, line_count DESC
213
+ `;
214
+ void sql; // suppress unused-variable warning for the filtered version above
215
+ const allSymbols = db
216
+ .prepare(sqlSimple)
217
+ .all(params);
218
+ // Exclude symbols that live in test files
219
+ const testFilePaths = new Set(testFiles.map((f) => f.path));
220
+ const sourceSymbols = allSymbols.filter((s) => !testFilePaths.has(s.file_path));
221
+ // ── Classify each symbol ──────────────────────────────────────────────────
222
+ const untestedSymbols = [];
223
+ let testedCount = 0;
224
+ // Cache file buffers for line computation
225
+ const fileBufferCache = new Map();
226
+ const getFileBuffer = (filePath) => {
227
+ if (fileBufferCache.has(filePath))
228
+ return fileBufferCache.get(filePath) ?? null;
229
+ const row = db
230
+ .prepare('SELECT raw_content FROM files WHERE repo_id = ? AND path = ?')
231
+ .get(repoId, filePath);
232
+ const buf = row?.raw_content ?? null;
233
+ if (buf)
234
+ fileBufferCache.set(filePath, buf);
235
+ return buf;
236
+ };
237
+ for (const sym of sourceSymbols) {
238
+ const isTested = allTestIdentifiers.has(sym.name);
239
+ if (isTested) {
240
+ testedCount++;
241
+ continue;
242
+ }
243
+ // Build start line
244
+ const buf = getFileBuffer(sym.file_path);
245
+ const startLine = buf ? lineOf(buf, sym.start_byte) : 1;
246
+ const cc = sym.cyclomatic_complexity ?? 1;
247
+ const lc = sym.line_count ?? 1;
248
+ const entry = {
249
+ symbolId: sym.id,
250
+ name: sym.name,
251
+ kind: sym.kind,
252
+ filePath: sym.file_path,
253
+ startLine,
254
+ signature: sym.signature,
255
+ summary: sym.summary,
256
+ lineCount: lc,
257
+ cyclomaticComplexity: cc,
258
+ priority: computePriority(cc, lc),
259
+ };
260
+ if (includeTestRefs) {
261
+ // For untested symbols, testRefs is empty (none reference it)
262
+ entry.testRefs = [];
263
+ }
264
+ untestedSymbols.push(entry);
265
+ }
266
+ const truncated = untestedSymbols.length > limit;
267
+ const visibleUntested = untestedSymbols.slice(0, limit);
268
+ // ── Summary ───────────────────────────────────────────────────────────────
269
+ const totalSymbols = sourceSymbols.length;
270
+ const untestedCount = untestedSymbols.length;
271
+ const testCoverageRate = totalSymbols > 0
272
+ ? Math.round(((testedCount) / totalSymbols) * 1000) / 1000
273
+ : 1;
274
+ const highCount = untestedSymbols.filter((s) => s.priority === 'high').length;
275
+ const mediumCount = untestedSymbols.filter((s) => s.priority === 'medium').length;
276
+ const lowCount = untestedSymbols.filter((s) => s.priority === 'low').length;
277
+ // ── Token estimate ─────────────────────────────────────────────────────────
278
+ const tokenEstimate = Math.ceil(visibleUntested.reduce((sum, s) => sum + s.signature.length + s.summary.length + s.filePath.length + 80, 0) / 4);
279
+ return {
280
+ content: [{
281
+ type: 'text',
282
+ text: JSON.stringify({
283
+ repoId,
284
+ totalSymbols,
285
+ testedCount,
286
+ untestedCount,
287
+ testCoverageRate,
288
+ testFilesScanned: testFiles.length,
289
+ truncated,
290
+ untestedSymbols: visibleUntested,
291
+ summary: {
292
+ highPriority: highCount,
293
+ mediumPriority: mediumCount,
294
+ lowPriority: lowCount,
295
+ kindsAnalyzed: kinds,
296
+ minLineCount,
297
+ },
298
+ _tokenEstimate: tokenEstimate,
299
+ _meta: buildMeta({ timingMs: Date.now() - t0 }),
300
+ }, null, 2),
301
+ }],
302
+ };
303
+ }
304
+ finally {
305
+ db.close();
306
+ }
307
+ }
308
+ //# sourceMappingURL=find-untested-symbols.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"find-untested-symbols.js","sourceRoot":"","sources":["../../../src/server/tools/find-untested-symbols.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * get-architecture-snapshot.ts
3
+ *
4
+ * MCP tool: get_architecture_snapshot
5
+ *
6
+ * Freeze the current architecture state (dep graph topology + quality metrics)
7
+ * into a named snapshot, and diff two snapshots to show what changed structurally
8
+ * between commits or dates.
9
+ *
10
+ * Actions:
11
+ * 'create' — compute current metrics, store as a snapshot row
12
+ * 'list' — return all snapshots for the repo, newest first
13
+ * 'diff' — compare two snapshot records and return deltas
14
+ * 'delete' — remove a snapshot by ID
15
+ *
16
+ * Storage: `snapshots` table in SQLite — stores aggregated metrics as a JSON
17
+ * blob plus the file list (for filesAdded/filesRemoved diffing).
18
+ * No full graph copies are stored.
19
+ */
20
+ import { z } from 'zod';
21
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
22
+ export declare const name = "get_architecture_snapshot";
23
+ export declare const description: string;
24
+ export declare const inputSchema: {
25
+ repoId: z.ZodString;
26
+ action: z.ZodEnum<{
27
+ delete: "delete";
28
+ create: "create";
29
+ diff: "diff";
30
+ list: "list";
31
+ }>;
32
+ snapshotId: z.ZodOptional<z.ZodString>;
33
+ compareId: z.ZodOptional<z.ZodString>;
34
+ label: z.ZodOptional<z.ZodString>;
35
+ };
36
+ export declare function handler(args: {
37
+ repoId: string;
38
+ action: 'create' | 'list' | 'diff' | 'delete';
39
+ snapshotId?: string;
40
+ compareId?: string;
41
+ label?: string;
42
+ }): Promise<CallToolResult>;
43
+ //# sourceMappingURL=get-architecture-snapshot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get-architecture-snapshot.d.ts","sourceRoot":"","sources":["../../../src/server/tools/get-architecture-snapshot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAEzE,eAAO,MAAM,IAAI,8BAA8B,CAAC;AAEhD,eAAO,MAAM,WAAW,QAOqB,CAAC;AAE9C,eAAO,MAAM,WAAW;;;;;;;;;;;CAiBvB,CAAC;AAsJF,wBAAsB,OAAO,CAAC,IAAI,EAAE;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAC;IAC9C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,GAAG,OAAO,CAAC,cAAc,CAAC,CAwM1B"}
@@ -0,0 +1,292 @@
1
+ /**
2
+ * get-architecture-snapshot.ts
3
+ *
4
+ * MCP tool: get_architecture_snapshot
5
+ *
6
+ * Freeze the current architecture state (dep graph topology + quality metrics)
7
+ * into a named snapshot, and diff two snapshots to show what changed structurally
8
+ * between commits or dates.
9
+ *
10
+ * Actions:
11
+ * 'create' — compute current metrics, store as a snapshot row
12
+ * 'list' — return all snapshots for the repo, newest first
13
+ * 'diff' — compare two snapshot records and return deltas
14
+ * 'delete' — remove a snapshot by ID
15
+ *
16
+ * Storage: `snapshots` table in SQLite — stores aggregated metrics as a JSON
17
+ * blob plus the file list (for filesAdded/filesRemoved diffing).
18
+ * No full graph copies are stored.
19
+ */
20
+ import { z } from 'zod';
21
+ import { randomBytes } from 'crypto';
22
+ import { openDatabase, getRepo } from '../../core/db/schema.js';
23
+ import { getCouplingMap } from '../../core/db/dep-store.js';
24
+ import { findImportCycles } from '../../graph/graph-traversal.js';
25
+ import { buildMeta } from './_meta.js';
26
+ export const name = 'get_architecture_snapshot';
27
+ export const description = 'Freeze the current architecture state into a named snapshot, then diff two snapshots ' +
28
+ 'to measure structural change over time. ' +
29
+ 'action "create": compute fileCount, symbolCount, edgeCount, cycleCount, avgCoupling, ' +
30
+ 'avgComplexity and store them with an optional label. ' +
31
+ 'action "list": return all snapshots for the repo, newest first. ' +
32
+ 'action "diff": compare two snapshots and return deltas (cycleCountDelta < 0 means fewer cycles — good). ' +
33
+ 'action "delete": remove a snapshot by ID.';
34
+ export const inputSchema = {
35
+ repoId: z.string().describe('Repo ID from index_folder or resolve_repo'),
36
+ action: z
37
+ .enum(['create', 'list', 'diff', 'delete'])
38
+ .describe('"create" | "list" | "diff" | "delete"'),
39
+ snapshotId: z
40
+ .string()
41
+ .optional()
42
+ .describe('Snapshot ID — required for diff (snapshotId = base) and delete'),
43
+ compareId: z
44
+ .string()
45
+ .optional()
46
+ .describe('The second snapshot ID to compare against — required for diff'),
47
+ label: z
48
+ .string()
49
+ .optional()
50
+ .describe('Human-readable label for the snapshot (e.g. "before-auth-refactor")'),
51
+ };
52
+ function ensureSnapshotsTable(db) {
53
+ db.exec(`
54
+ CREATE TABLE IF NOT EXISTS snapshots (
55
+ snapshot_id TEXT NOT NULL,
56
+ repo_id TEXT NOT NULL,
57
+ label TEXT NOT NULL DEFAULT '',
58
+ created_at INTEGER NOT NULL,
59
+ metrics TEXT NOT NULL,
60
+ PRIMARY KEY (snapshot_id, repo_id),
61
+ FOREIGN KEY (repo_id) REFERENCES repos(id) ON DELETE CASCADE
62
+ )
63
+ `);
64
+ try {
65
+ db.exec('CREATE INDEX IF NOT EXISTS idx_snapshots_repo ON snapshots(repo_id, created_at)');
66
+ }
67
+ catch {
68
+ // already exists
69
+ }
70
+ }
71
+ function rowToRecord(row) {
72
+ const m = JSON.parse(row.metrics);
73
+ return {
74
+ snapshotId: row.snapshot_id,
75
+ label: row.label,
76
+ createdAt: new Date(row.created_at).toISOString(),
77
+ fileCount: m.fileCount,
78
+ symbolCount: m.symbolCount,
79
+ edgeCount: m.edgeCount,
80
+ cycleCount: m.cycleCount,
81
+ avgCoupling: m.avgCoupling,
82
+ avgComplexity: m.avgComplexity,
83
+ };
84
+ }
85
+ // ─── Metrics computation ──────────────────────────────────────────────────────
86
+ function computeMetrics(db, repoId) {
87
+ // File count
88
+ const fileRow = db
89
+ .prepare('SELECT COUNT(*) AS n FROM files WHERE repo_id = ?')
90
+ .get(repoId);
91
+ const fileCount = fileRow?.n ?? 0;
92
+ // Symbol count
93
+ const symRow = db
94
+ .prepare('SELECT COUNT(*) AS n FROM symbols WHERE repo_id = ?')
95
+ .get(repoId);
96
+ const symbolCount = symRow?.n ?? 0;
97
+ // Edge count
98
+ const edgeRow = db
99
+ .prepare('SELECT COUNT(*) AS n FROM dep_edges WHERE repo_id = ?')
100
+ .get(repoId);
101
+ const edgeCount = edgeRow?.n ?? 0;
102
+ // File paths list
103
+ const filePaths = db
104
+ .prepare('SELECT path FROM files WHERE repo_id = ? ORDER BY path')
105
+ .all(repoId)
106
+ .map((r) => r.path);
107
+ // Cycle count — use DFS-based findImportCycles, cap at 500
108
+ const cyclesResult = findImportCycles(repoId, db, undefined, 500);
109
+ const cycleCount = cyclesResult.cycles.length;
110
+ // Average coupling (efferent + afferent per file / 2 averaged across files)
111
+ const couplingRows = getCouplingMap(db, repoId);
112
+ let avgCoupling = 0;
113
+ if (couplingRows.length > 0) {
114
+ const totalCoupling = couplingRows.reduce((sum, r) => sum + r.efferentCoupling + r.afferentCoupling, 0);
115
+ avgCoupling = Math.round((totalCoupling / couplingRows.length) * 100) / 100;
116
+ }
117
+ // Average cyclomatic complexity from indexed symbols (skip NULLs)
118
+ const complexityRow = db
119
+ .prepare('SELECT AVG(cyclomatic_complexity) AS avg FROM symbols WHERE repo_id = ? AND cyclomatic_complexity IS NOT NULL')
120
+ .get(repoId);
121
+ const avgComplexity = complexityRow?.avg != null
122
+ ? Math.round(complexityRow.avg * 100) / 100
123
+ : 0;
124
+ return { fileCount, symbolCount, edgeCount, cycleCount, avgCoupling, avgComplexity, files: filePaths };
125
+ }
126
+ // ─── Handler ──────────────────────────────────────────────────────────────────
127
+ export async function handler(args) {
128
+ const t0 = Date.now();
129
+ const { repoId, action, snapshotId, compareId, label = '' } = args;
130
+ const db = openDatabase(repoId);
131
+ try {
132
+ const repo = getRepo(db, repoId);
133
+ if (!repo) {
134
+ return {
135
+ content: [
136
+ {
137
+ type: 'text',
138
+ text: JSON.stringify({ error: `Repo "${repoId}" not found. Run index_folder first.` }),
139
+ },
140
+ ],
141
+ isError: true,
142
+ };
143
+ }
144
+ // Ensure the snapshots table exists (safe with IF NOT EXISTS)
145
+ ensureSnapshotsTable(db);
146
+ if (action === 'create') {
147
+ const metrics = computeMetrics(db, repoId);
148
+ const snapId = randomBytes(6).toString('hex'); // 12-char hex
149
+ const now = Date.now();
150
+ db.prepare(`
151
+ INSERT INTO snapshots (snapshot_id, repo_id, label, created_at, metrics)
152
+ VALUES (?, ?, ?, ?, ?)
153
+ `).run(snapId, repoId, label, now, JSON.stringify(metrics));
154
+ const record = {
155
+ snapshotId: snapId,
156
+ label,
157
+ createdAt: new Date(now).toISOString(),
158
+ fileCount: metrics.fileCount,
159
+ symbolCount: metrics.symbolCount,
160
+ edgeCount: metrics.edgeCount,
161
+ cycleCount: metrics.cycleCount,
162
+ avgCoupling: metrics.avgCoupling,
163
+ avgComplexity: metrics.avgComplexity,
164
+ };
165
+ const output = {
166
+ action: 'create',
167
+ snapshot: record,
168
+ _meta: buildMeta({ timingMs: Date.now() - t0 }),
169
+ };
170
+ return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
171
+ }
172
+ if (action === 'list') {
173
+ const rows = db
174
+ .prepare('SELECT * FROM snapshots WHERE repo_id = ? ORDER BY created_at DESC')
175
+ .all(repoId);
176
+ const output = {
177
+ action: 'list',
178
+ snapshots: rows.map(rowToRecord),
179
+ _meta: buildMeta({ timingMs: Date.now() - t0 }),
180
+ };
181
+ return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
182
+ }
183
+ if (action === 'diff') {
184
+ if (!snapshotId || !compareId) {
185
+ return {
186
+ content: [
187
+ {
188
+ type: 'text',
189
+ text: JSON.stringify({
190
+ error: 'action "diff" requires both snapshotId (base) and compareId (compare).',
191
+ }),
192
+ },
193
+ ],
194
+ isError: true,
195
+ };
196
+ }
197
+ const baseRow = db
198
+ .prepare('SELECT * FROM snapshots WHERE snapshot_id = ? AND repo_id = ?')
199
+ .get(snapshotId, repoId);
200
+ const compareRow = db
201
+ .prepare('SELECT * FROM snapshots WHERE snapshot_id = ? AND repo_id = ?')
202
+ .get(compareId, repoId);
203
+ if (!baseRow) {
204
+ return {
205
+ content: [
206
+ {
207
+ type: 'text',
208
+ text: JSON.stringify({ error: `Snapshot "${snapshotId}" not found.` }),
209
+ },
210
+ ],
211
+ isError: true,
212
+ };
213
+ }
214
+ if (!compareRow) {
215
+ return {
216
+ content: [
217
+ {
218
+ type: 'text',
219
+ text: JSON.stringify({ error: `Snapshot "${compareId}" not found.` }),
220
+ },
221
+ ],
222
+ isError: true,
223
+ };
224
+ }
225
+ const baseMetrics = JSON.parse(baseRow.metrics);
226
+ const compareMetrics = JSON.parse(compareRow.metrics);
227
+ const baseFiles = new Set(baseMetrics.files);
228
+ const compareFiles = new Set(compareMetrics.files);
229
+ const filesAdded = compareMetrics.files.filter((f) => !baseFiles.has(f));
230
+ const filesRemoved = baseMetrics.files.filter((f) => !compareFiles.has(f));
231
+ const diff = {
232
+ filesAdded,
233
+ filesRemoved,
234
+ symbolsAdded: Math.max(0, compareMetrics.symbolCount - baseMetrics.symbolCount),
235
+ symbolsRemoved: Math.max(0, baseMetrics.symbolCount - compareMetrics.symbolCount),
236
+ edgesAdded: Math.max(0, compareMetrics.edgeCount - baseMetrics.edgeCount),
237
+ edgesRemoved: Math.max(0, baseMetrics.edgeCount - compareMetrics.edgeCount),
238
+ cycleCountDelta: compareMetrics.cycleCount - baseMetrics.cycleCount,
239
+ avgCouplingDelta: Math.round((compareMetrics.avgCoupling - baseMetrics.avgCoupling) * 100) / 100,
240
+ avgComplexityDelta: Math.round((compareMetrics.avgComplexity - baseMetrics.avgComplexity) * 100) / 100,
241
+ };
242
+ const output = {
243
+ action: 'diff',
244
+ snapshot: rowToRecord(baseRow),
245
+ diff,
246
+ _meta: buildMeta({ timingMs: Date.now() - t0 }),
247
+ };
248
+ return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
249
+ }
250
+ if (action === 'delete') {
251
+ if (!snapshotId) {
252
+ return {
253
+ content: [
254
+ {
255
+ type: 'text',
256
+ text: JSON.stringify({ error: 'action "delete" requires snapshotId.' }),
257
+ },
258
+ ],
259
+ isError: true,
260
+ };
261
+ }
262
+ const result = db
263
+ .prepare('DELETE FROM snapshots WHERE snapshot_id = ? AND repo_id = ?')
264
+ .run(snapshotId, repoId);
265
+ if (result.changes === 0) {
266
+ return {
267
+ content: [
268
+ {
269
+ type: 'text',
270
+ text: JSON.stringify({ error: `Snapshot "${snapshotId}" not found.` }),
271
+ },
272
+ ],
273
+ isError: true,
274
+ };
275
+ }
276
+ const output = {
277
+ action: 'delete',
278
+ _meta: buildMeta({ timingMs: Date.now() - t0 }),
279
+ };
280
+ return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
281
+ }
282
+ // Should never reach here (Zod enum validation prevents it)
283
+ return {
284
+ content: [{ type: 'text', text: JSON.stringify({ error: `Unknown action: ${action}` }) }],
285
+ isError: true,
286
+ };
287
+ }
288
+ finally {
289
+ db.close();
290
+ }
291
+ }
292
+ //# sourceMappingURL=get-architecture-snapshot.js.map