ucn 3.8.23 → 3.8.26
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/.claude/skills/ucn/SKILL.md +127 -12
- package/README.md +152 -156
- package/cli/index.js +363 -37
- package/core/analysis.js +936 -32
- package/core/bridge.js +1095 -0
- package/core/brief.js +408 -0
- package/core/cache.js +105 -5
- package/core/callers.js +72 -18
- package/core/check.js +200 -0
- package/core/discovery.js +57 -34
- package/core/entrypoints.js +638 -4
- package/core/execute.js +304 -5
- package/core/git-enrich.js +130 -0
- package/core/graph.js +24 -2
- package/core/output/analysis.js +157 -25
- package/core/output/brief.js +100 -0
- package/core/output/check.js +79 -0
- package/core/output/doctor.js +85 -0
- package/core/output/endpoints.js +239 -0
- package/core/output/extraction.js +2 -0
- package/core/output/find.js +126 -39
- package/core/output/graph.js +48 -15
- package/core/output/refactoring.js +103 -5
- package/core/output/reporting.js +63 -23
- package/core/output/search.js +110 -17
- package/core/output/shared.js +56 -2
- package/core/output.js +4 -0
- package/core/parser.js +8 -2
- package/core/project.js +39 -3
- package/core/registry.js +30 -14
- package/core/reporting.js +465 -2
- package/core/search.js +130 -52
- package/core/shared.js +101 -5
- package/core/tracing.js +16 -6
- package/core/verify.js +982 -95
- package/languages/go.js +91 -6
- package/languages/html.js +10 -0
- package/languages/java.js +151 -35
- package/languages/javascript.js +290 -33
- package/languages/python.js +78 -11
- package/languages/rust.js +267 -12
- package/languages/utils.js +315 -3
- package/mcp/server.js +91 -16
- package/package.json +9 -1
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/output/endpoints.js — Formatters for the `endpoints` command.
|
|
3
|
+
*
|
|
4
|
+
* Two surfaces: text (human) and JSON (machine).
|
|
5
|
+
*
|
|
6
|
+
* Layout principles:
|
|
7
|
+
* - Routes grouped by file when no --bridge flag.
|
|
8
|
+
* - With --bridge: routes listed individually, each followed by matched clients
|
|
9
|
+
* and a confidence tag, then unmatched routes/requests.
|
|
10
|
+
* - Counts at the top so the user knows the size up front.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const SEP_TIER = { exact: 'EXACT', partial: 'PARTIAL', uncertain: 'UNCERTAIN' };
|
|
16
|
+
|
|
17
|
+
function pad(s, n) {
|
|
18
|
+
if (typeof s !== 'string') s = String(s);
|
|
19
|
+
return s.length >= n ? s : s + ' '.repeat(n - s.length);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function formatEndpoints(result, options = {}) {
|
|
23
|
+
if (!result) return 'No endpoints data.';
|
|
24
|
+
const { routes, requests, bridges, unmatchedRoutes, unmatchedRequests, meta } = result;
|
|
25
|
+
const showBridge = options.bridge;
|
|
26
|
+
|
|
27
|
+
if (!showBridge) {
|
|
28
|
+
return formatRoutesAndRequests(routes, requests, meta, options);
|
|
29
|
+
}
|
|
30
|
+
return formatBridges(bridges, unmatchedRoutes, unmatchedRequests, meta, options);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Compute the unique-request match percentage. A single client request that
|
|
35
|
+
* matches multiple server routes (e.g., trailing-slash dups) must count once,
|
|
36
|
+
* not once per bridge — otherwise "Matched: N (200%)" shows up. Returns an
|
|
37
|
+
* integer percentage clamped to [0, 100].
|
|
38
|
+
*/
|
|
39
|
+
function uniqueMatchPercent(bridges, totalRequests) {
|
|
40
|
+
if (!totalRequests || totalRequests <= 0) return 0;
|
|
41
|
+
const matchedRequestKeys = new Set();
|
|
42
|
+
for (const b of bridges) {
|
|
43
|
+
const r = b.request;
|
|
44
|
+
matchedRequestKeys.add(`${r.absoluteFile || r.file}:${r.line}:${r.method}:${r.path}`);
|
|
45
|
+
}
|
|
46
|
+
const pct = Math.round((matchedRequestKeys.size / totalRequests) * 100);
|
|
47
|
+
return Math.min(100, Math.max(0, pct));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function formatRoutesAndRequests(routes, requests, meta, options) {
|
|
51
|
+
const lines = [];
|
|
52
|
+
const showServer = !options.clientOnly;
|
|
53
|
+
const showClient = !options.serverOnly;
|
|
54
|
+
|
|
55
|
+
if (showServer) {
|
|
56
|
+
if (routes.length === 0) {
|
|
57
|
+
lines.push('No server routes detected.');
|
|
58
|
+
} else {
|
|
59
|
+
lines.push(`Server Routes: ${routes.length}`);
|
|
60
|
+
const fwSummary = Object.entries(meta.byFramework || {})
|
|
61
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
62
|
+
.join(', ');
|
|
63
|
+
if (fwSummary) lines.push(`Frameworks: ${fwSummary}`);
|
|
64
|
+
lines.push('');
|
|
65
|
+
|
|
66
|
+
const byFile = new Map();
|
|
67
|
+
for (const r of routes) {
|
|
68
|
+
const list = byFile.get(r.file) || [];
|
|
69
|
+
list.push(r);
|
|
70
|
+
byFile.set(r.file, list);
|
|
71
|
+
}
|
|
72
|
+
for (const [file, list] of byFile) {
|
|
73
|
+
lines.push(`${file}`);
|
|
74
|
+
for (const r of list) {
|
|
75
|
+
const handler = r.handler || '<anonymous>';
|
|
76
|
+
const fw = r.framework ? `[${r.framework}]` : '';
|
|
77
|
+
lines.push(` ${pad(r.method, 7)} ${pad(r.path, 40)} → ${handler} ${fw} :${r.line}`);
|
|
78
|
+
}
|
|
79
|
+
lines.push('');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (showClient) {
|
|
85
|
+
if (requests.length === 0) {
|
|
86
|
+
if (showServer) lines.push('No client requests detected.');
|
|
87
|
+
} else {
|
|
88
|
+
if (showServer) lines.push('');
|
|
89
|
+
lines.push(`Client Requests: ${requests.length}`);
|
|
90
|
+
lines.push('');
|
|
91
|
+
|
|
92
|
+
const byFile = new Map();
|
|
93
|
+
for (const r of requests) {
|
|
94
|
+
const list = byFile.get(r.file) || [];
|
|
95
|
+
list.push(r);
|
|
96
|
+
byFile.set(r.file, list);
|
|
97
|
+
}
|
|
98
|
+
for (const [file, list] of byFile) {
|
|
99
|
+
lines.push(`${file}`);
|
|
100
|
+
for (const r of list) {
|
|
101
|
+
const inferred = r.methodInferred ? '?' : '';
|
|
102
|
+
const interp = r.interp ? ' (interp)' : '';
|
|
103
|
+
const fw = r.framework ? `[${r.framework}]` : '';
|
|
104
|
+
lines.push(` ${pad(r.method + inferred, 7)} ${pad(r.path + interp, 40)} from ${r.callerName} ${fw} :${r.line}`);
|
|
105
|
+
}
|
|
106
|
+
lines.push('');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return lines.join('\n').trimEnd();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function formatBridges(bridges, unmatchedRoutes, unmatchedRequests, meta, options = {}) {
|
|
115
|
+
const lines = [];
|
|
116
|
+
const matched = bridges.length;
|
|
117
|
+
const unmatchedOnly = !!options.unmatched;
|
|
118
|
+
|
|
119
|
+
lines.push(`Endpoint Bridges`);
|
|
120
|
+
lines.push(`================`);
|
|
121
|
+
lines.push(`Server routes: ${meta.totalRoutes} Client requests: ${meta.totalRequests}`);
|
|
122
|
+
// HIGH-3: percentage = unique matched client requests / total client
|
|
123
|
+
// requests. Counting bridges directly inflates >100% on many-to-many
|
|
124
|
+
// matches (trailing-slash dups, wildcard overlap, etc.).
|
|
125
|
+
const pct = uniqueMatchPercent(bridges, meta.totalRequests);
|
|
126
|
+
lines.push(`Matched: ${matched} (${pct}%) Unmatched routes: ${unmatchedRoutes.length} Unmatched requests: ${unmatchedRequests.length}`);
|
|
127
|
+
lines.push('');
|
|
128
|
+
|
|
129
|
+
// HIGH-2: in --unmatched mode, suppress the Matched section entirely.
|
|
130
|
+
if (matched > 0 && !unmatchedOnly) {
|
|
131
|
+
// Group bridges by route for display
|
|
132
|
+
const byRoute = new Map();
|
|
133
|
+
for (const b of bridges) {
|
|
134
|
+
const key = `${b.route.absoluteFile}:${b.route.line}:${b.route.method}:${b.route.path}`;
|
|
135
|
+
const list = byRoute.get(key) || { route: b.route, clients: [] };
|
|
136
|
+
list.clients.push(b);
|
|
137
|
+
byRoute.set(key, list);
|
|
138
|
+
}
|
|
139
|
+
// Sort routes alphabetically
|
|
140
|
+
const sorted = [...byRoute.values()].sort((a, b) => {
|
|
141
|
+
if (a.route.file !== b.route.file) return a.route.file.localeCompare(b.route.file);
|
|
142
|
+
return a.route.line - b.route.line;
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
lines.push(`Matched (${sorted.length} routes):`);
|
|
146
|
+
for (const { route, clients } of sorted) {
|
|
147
|
+
lines.push(` ${pad(route.method, 7)} ${route.path} [${route.framework}] ${route.file}:${route.line}`);
|
|
148
|
+
for (const b of clients) {
|
|
149
|
+
const conf = b.confidence.toFixed(2);
|
|
150
|
+
const tier = b.matchType.toUpperCase();
|
|
151
|
+
const inf = b.methodInferred ? ' method?' : '';
|
|
152
|
+
lines.push(` ↔ ${pad(b.request.method + inf, 9)} ${pad(b.request.path, 30)} ${tier} (${conf}) from ${b.request.callerName} ${b.request.file}:${b.request.line}`);
|
|
153
|
+
}
|
|
154
|
+
lines.push('');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (unmatchedRoutes.length > 0) {
|
|
159
|
+
lines.push(`Unmatched server routes (${unmatchedRoutes.length}):`);
|
|
160
|
+
for (const r of unmatchedRoutes) {
|
|
161
|
+
lines.push(` ${pad(r.method, 7)} ${pad(r.path, 40)} → ${r.handler} [${r.framework}] ${r.file}:${r.line}`);
|
|
162
|
+
}
|
|
163
|
+
lines.push('');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (unmatchedRequests.length > 0) {
|
|
167
|
+
lines.push(`Unmatched client requests (${unmatchedRequests.length}):`);
|
|
168
|
+
for (const r of unmatchedRequests) {
|
|
169
|
+
const inferred = r.methodInferred ? '?' : '';
|
|
170
|
+
const interp = r.interp ? ' (interp)' : '';
|
|
171
|
+
lines.push(` ${pad(r.method + inferred, 7)} ${pad(r.path + interp, 40)} from ${r.callerName} [${r.framework}] ${r.file}:${r.line}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return lines.join('\n').trimEnd();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function formatEndpointsJson(result, options = {}) {
|
|
179
|
+
if (!result) return JSON.stringify({ meta: {}, data: {} }, null, 2);
|
|
180
|
+
const { routes, requests, bridges, unmatchedRoutes, unmatchedRequests, meta } = result;
|
|
181
|
+
// Read `unmatched` from explicit options OR sticky result property (set by
|
|
182
|
+
// execute.js when the user passed --unmatched). The CLI/MCP wrappers may
|
|
183
|
+
// not pass options through to JSON output, so we use both as fallbacks.
|
|
184
|
+
const unmatchedOnly = !!(options.unmatched || result._unmatched);
|
|
185
|
+
|
|
186
|
+
const trimRoute = (r) => ({
|
|
187
|
+
method: r.method,
|
|
188
|
+
path: r.path,
|
|
189
|
+
normalizedPath: r.normalizedPath,
|
|
190
|
+
handler: r.handler,
|
|
191
|
+
file: r.file,
|
|
192
|
+
line: r.line,
|
|
193
|
+
framework: r.framework,
|
|
194
|
+
...(r.classPrefix && { classPrefix: r.classPrefix }),
|
|
195
|
+
});
|
|
196
|
+
const trimReq = (r) => ({
|
|
197
|
+
method: r.method,
|
|
198
|
+
path: r.path,
|
|
199
|
+
normalizedPath: r.normalizedPath,
|
|
200
|
+
...(r.interp && { interp: true }),
|
|
201
|
+
file: r.file,
|
|
202
|
+
line: r.line,
|
|
203
|
+
callerName: r.callerName,
|
|
204
|
+
...(r.callerStartLine && { callerStartLine: r.callerStartLine }),
|
|
205
|
+
framework: r.framework,
|
|
206
|
+
...(r.methodInferred && { methodInferred: true }),
|
|
207
|
+
});
|
|
208
|
+
const trimBridge = (b) => ({
|
|
209
|
+
route: trimRoute(b.route),
|
|
210
|
+
request: trimReq(b.request),
|
|
211
|
+
matchType: b.matchType,
|
|
212
|
+
confidence: b.confidence,
|
|
213
|
+
...(b.methodInferred && { methodInferred: true }),
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
return JSON.stringify({
|
|
217
|
+
meta: {
|
|
218
|
+
ok: true,
|
|
219
|
+
...meta,
|
|
220
|
+
// HIGH-2: signal to consumers that bridges array was suppressed
|
|
221
|
+
// because the user filtered to unmatched-only.
|
|
222
|
+
...(unmatchedOnly && { filterMode: 'unmatched' }),
|
|
223
|
+
},
|
|
224
|
+
data: {
|
|
225
|
+
routes: routes.map(trimRoute),
|
|
226
|
+
requests: requests.map(trimReq),
|
|
227
|
+
// In unmatched-only mode, the matched bridges array is suppressed
|
|
228
|
+
// — consumers that want both should not pass --unmatched.
|
|
229
|
+
bridges: unmatchedOnly ? [] : bridges.map(trimBridge),
|
|
230
|
+
unmatchedRoutes: unmatchedRoutes.map(trimRoute),
|
|
231
|
+
unmatchedRequests: unmatchedRequests.map(trimReq),
|
|
232
|
+
},
|
|
233
|
+
}, null, 2);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
module.exports = {
|
|
237
|
+
formatEndpoints,
|
|
238
|
+
formatEndpointsJson,
|
|
239
|
+
};
|
|
@@ -45,6 +45,7 @@ function formatFunctionJson(fn, code) {
|
|
|
45
45
|
endLine: fn.endLine,
|
|
46
46
|
modifiers: fn.modifiers || [],
|
|
47
47
|
...(fn.returnType && { returnType: fn.returnType }),
|
|
48
|
+
...(fn.paramTypes && { paramTypes: fn.paramTypes }),
|
|
48
49
|
...(fn.generics && { generics: fn.generics }),
|
|
49
50
|
...(fn.docstring && { docstring: fn.docstring }),
|
|
50
51
|
...(fn.isArrow && { isArrow: true }),
|
|
@@ -82,6 +83,7 @@ function formatFnResultJson(result) {
|
|
|
82
83
|
endLine: match.endLine,
|
|
83
84
|
modifiers: match.modifiers || [],
|
|
84
85
|
...(match.returnType && { returnType: match.returnType }),
|
|
86
|
+
...(match.paramTypes && { paramTypes: match.paramTypes }),
|
|
85
87
|
...(match.generics && { generics: match.generics }),
|
|
86
88
|
...(match.docstring && { docstring: match.docstring }),
|
|
87
89
|
...(match.isArrow && { isArrow: true }),
|
package/core/output/find.js
CHANGED
|
@@ -9,6 +9,20 @@ const {
|
|
|
9
9
|
computeConfidence,
|
|
10
10
|
} = require('./shared');
|
|
11
11
|
|
|
12
|
+
const { formatSymbolHandle } = require('../shared');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Trim a docstring to a single short sentence (max 80 chars) for inline display.
|
|
16
|
+
*/
|
|
17
|
+
function firstSentenceShort(text) {
|
|
18
|
+
if (!text) return null;
|
|
19
|
+
const trimmed = text.trim();
|
|
20
|
+
const m = trimmed.match(/^(.+?[.!?])(?:\s|$)/);
|
|
21
|
+
let s = m ? m[1] : trimmed;
|
|
22
|
+
if (s.length > 80) s = s.slice(0, 77) + '...';
|
|
23
|
+
return s;
|
|
24
|
+
}
|
|
25
|
+
|
|
12
26
|
/**
|
|
13
27
|
* Format find command output
|
|
14
28
|
*/
|
|
@@ -34,6 +48,10 @@ function formatFind(symbols, query, top) {
|
|
|
34
48
|
? formatFunctionSignature(s)
|
|
35
49
|
: formatClassSignature(s);
|
|
36
50
|
lines.push(`${s.relativePath}:${s.startLine} ${sig}`);
|
|
51
|
+
if (s.docstring) {
|
|
52
|
+
const snip = firstSentenceShort(s.docstring);
|
|
53
|
+
if (snip) lines.push(` "${snip}"`);
|
|
54
|
+
}
|
|
37
55
|
if (s.usageCounts !== undefined) {
|
|
38
56
|
const c = s.usageCounts;
|
|
39
57
|
const parts = [];
|
|
@@ -57,6 +75,7 @@ function formatFind(symbols, query, top) {
|
|
|
57
75
|
}
|
|
58
76
|
|
|
59
77
|
function formatFindJson(items) {
|
|
78
|
+
const { formatSymbolHandle } = require('../shared');
|
|
60
79
|
return JSON.stringify({
|
|
61
80
|
meta: { command: 'find', count: items.length },
|
|
62
81
|
data: items.map(m => ({
|
|
@@ -65,6 +84,7 @@ function formatFindJson(items) {
|
|
|
65
84
|
file: m.relativePath || m.file,
|
|
66
85
|
line: m.startLine,
|
|
67
86
|
endLine: m.endLine,
|
|
87
|
+
handle: formatSymbolHandle(m),
|
|
68
88
|
...(m.className && { className: m.className }),
|
|
69
89
|
...(m.receiver && { receiver: m.receiver }),
|
|
70
90
|
})),
|
|
@@ -80,7 +100,7 @@ function formatFindJson(items) {
|
|
|
80
100
|
* @param {object} options - { depth, top, all }
|
|
81
101
|
*/
|
|
82
102
|
function formatFindDetailed(symbols, query, options = {}) {
|
|
83
|
-
const { depth, top, all } = options;
|
|
103
|
+
const { depth, top, all, compact } = options;
|
|
84
104
|
const DEFAULT_LIMIT = 5;
|
|
85
105
|
|
|
86
106
|
if (symbols.length === 0) {
|
|
@@ -97,7 +117,7 @@ function formatFindDetailed(symbols, query, options = {}) {
|
|
|
97
117
|
} else {
|
|
98
118
|
lines.push(`Found ${symbols.length} match(es) for "${query}":`);
|
|
99
119
|
}
|
|
100
|
-
lines.push('─'.repeat(60));
|
|
120
|
+
if (!compact) lines.push('─'.repeat(60));
|
|
101
121
|
|
|
102
122
|
for (let i = 0; i < showing; i++) {
|
|
103
123
|
const s = symbols[i];
|
|
@@ -114,8 +134,30 @@ function formatFindDetailed(symbols, query, options = {}) {
|
|
|
114
134
|
|
|
115
135
|
const confidence = computeConfidence(s);
|
|
116
136
|
const confStr = confidence.level !== 'high' ? ` [${confidence.level}]` : '';
|
|
137
|
+
const handle = formatSymbolHandle(s);
|
|
138
|
+
const loc = handle || (s.relativePath + ':' + s.startLine);
|
|
139
|
+
|
|
140
|
+
if (compact) {
|
|
141
|
+
// One line per result: "<handle> <sig> <usages?> <doc snippet?>"
|
|
142
|
+
const parts = [`${loc} ${sig}${confStr}`];
|
|
143
|
+
if (s.usageCounts !== undefined && s.usageCounts.total > 0) {
|
|
144
|
+
parts.push(`(${s.usageCounts.total} usages)`);
|
|
145
|
+
} else if (s.usageCount !== undefined) {
|
|
146
|
+
parts.push(`(${s.usageCount} usages)`);
|
|
147
|
+
}
|
|
148
|
+
if (s.docstring) {
|
|
149
|
+
const snip = firstSentenceShort(s.docstring);
|
|
150
|
+
if (snip) parts.push(`— ${snip}`);
|
|
151
|
+
}
|
|
152
|
+
lines.push(parts.join(' '));
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
117
155
|
|
|
118
|
-
lines.push(`${
|
|
156
|
+
lines.push(`${loc} ${sig}${confStr}`);
|
|
157
|
+
if (s.docstring) {
|
|
158
|
+
const snip = firstSentenceShort(s.docstring);
|
|
159
|
+
if (snip) lines.push(` "${snip}"`);
|
|
160
|
+
}
|
|
119
161
|
if (s.usageCounts !== undefined) {
|
|
120
162
|
const c = s.usageCounts;
|
|
121
163
|
const parts = [];
|
|
@@ -150,7 +192,7 @@ function formatFindDetailed(symbols, query, options = {}) {
|
|
|
150
192
|
// Skip code extraction on error
|
|
151
193
|
}
|
|
152
194
|
}
|
|
153
|
-
lines.push('');
|
|
195
|
+
if (!compact) lines.push('');
|
|
154
196
|
}
|
|
155
197
|
|
|
156
198
|
if (hidden > 0) {
|
|
@@ -164,6 +206,7 @@ function formatFindDetailed(symbols, query, options = {}) {
|
|
|
164
206
|
* Format symbol search results as JSON
|
|
165
207
|
*/
|
|
166
208
|
function formatSymbolJson(symbols, query) {
|
|
209
|
+
const { formatSymbolHandle } = require('../shared');
|
|
167
210
|
return JSON.stringify({
|
|
168
211
|
meta: { complete: true, skipped: 0, dynamicImports: 0, uncertain: 0 },
|
|
169
212
|
data: {
|
|
@@ -175,9 +218,12 @@ function formatSymbolJson(symbols, query) {
|
|
|
175
218
|
file: s.relativePath || s.file,
|
|
176
219
|
startLine: s.startLine,
|
|
177
220
|
endLine: s.endLine,
|
|
221
|
+
handle: formatSymbolHandle(s),
|
|
178
222
|
...(s.params && { params: s.params }), // FULL params
|
|
179
223
|
...(s.paramsStructured && { paramsStructured: s.paramsStructured }),
|
|
180
224
|
...(s.returnType && { returnType: s.returnType }),
|
|
225
|
+
...(s.paramTypes && { paramTypes: s.paramTypes }),
|
|
226
|
+
...(s.docstring && { docstring: s.docstring }),
|
|
181
227
|
...(s.modifiers && { modifiers: s.modifiers }),
|
|
182
228
|
...(s.usageCount !== undefined && { usageCount: s.usageCount }),
|
|
183
229
|
...(s.usageCounts !== undefined && { usageCounts: s.usageCounts })
|
|
@@ -190,6 +236,7 @@ function formatSymbolJson(symbols, query) {
|
|
|
190
236
|
* Format usages as JSON - FULL expressions, never truncated
|
|
191
237
|
*/
|
|
192
238
|
function formatUsagesJson(usages, name) {
|
|
239
|
+
const { formatSymbolHandle } = require('../shared');
|
|
193
240
|
const definitions = usages.filter(u => u.isDefinition);
|
|
194
241
|
const refs = usages.filter(u => !u.isDefinition);
|
|
195
242
|
|
|
@@ -197,14 +244,29 @@ function formatUsagesJson(usages, name) {
|
|
|
197
244
|
const imports = refs.filter(u => u.usageType === 'import');
|
|
198
245
|
const references = refs.filter(u => u.usageType === 'reference');
|
|
199
246
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
247
|
+
// Each usage record points at a call site. We emit a per-occurrence handle
|
|
248
|
+
// pointing at the SITE itself in the form "relativePath:line:callerName"
|
|
249
|
+
// (or "relativePath:line:_topLevel" when the call is at module scope and has
|
|
250
|
+
// no enclosing function). When the enclosing function position is known, we
|
|
251
|
+
// also emit `enclosingHandle` as a jump-back target to the function head.
|
|
252
|
+
const formatUsage = (u) => {
|
|
253
|
+
const file = u.relativePath || u.file;
|
|
254
|
+
const callerToken = u.callerName || '_topLevel';
|
|
255
|
+
const handle = `${file}:${u.line}:${callerToken}`;
|
|
256
|
+
const enclosingHandle = (u.callerStartLine && u.callerName)
|
|
257
|
+
? `${file}:${u.callerStartLine}:${u.callerName}`
|
|
258
|
+
: undefined;
|
|
259
|
+
return {
|
|
260
|
+
file,
|
|
261
|
+
line: u.line,
|
|
262
|
+
handle,
|
|
263
|
+
...(enclosingHandle && { enclosingHandle }),
|
|
264
|
+
expression: u.content, // FULL expression - key improvement
|
|
265
|
+
...(u.args && { args: u.args }), // Parsed arguments
|
|
266
|
+
...(u.before && u.before.length > 0 && { before: u.before }),
|
|
267
|
+
...(u.after && u.after.length > 0 && { after: u.after })
|
|
268
|
+
};
|
|
269
|
+
};
|
|
208
270
|
|
|
209
271
|
return JSON.stringify({
|
|
210
272
|
meta: { complete: true, skipped: 0, dynamicImports: 0, uncertain: 0 },
|
|
@@ -215,15 +277,20 @@ function formatUsagesJson(usages, name) {
|
|
|
215
277
|
importCount: imports.length,
|
|
216
278
|
referenceCount: references.length,
|
|
217
279
|
totalUsages: refs.length,
|
|
218
|
-
definitions: definitions.map(d =>
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
280
|
+
definitions: definitions.map(d => {
|
|
281
|
+
const handle = formatSymbolHandle({ ...d, name: d.name || name });
|
|
282
|
+
return {
|
|
283
|
+
file: d.relativePath || d.file,
|
|
284
|
+
line: d.line,
|
|
285
|
+
...(handle && { handle }),
|
|
286
|
+
signature: d.signature || null, // FULL signature
|
|
287
|
+
type: d.type || null,
|
|
288
|
+
...(d.returnType && { returnType: d.returnType }),
|
|
289
|
+
...(d.docstring && { docstring: d.docstring }),
|
|
290
|
+
...(d.before && d.before.length > 0 && { before: d.before }),
|
|
291
|
+
...(d.after && d.after.length > 0 && { after: d.after })
|
|
292
|
+
};
|
|
293
|
+
}),
|
|
227
294
|
calls: calls.map(formatUsage),
|
|
228
295
|
imports: imports.map(formatUsage),
|
|
229
296
|
references: references.map(formatUsage)
|
|
@@ -234,7 +301,8 @@ function formatUsagesJson(usages, name) {
|
|
|
234
301
|
/**
|
|
235
302
|
* Format usages command output
|
|
236
303
|
*/
|
|
237
|
-
function formatUsages(usages, name) {
|
|
304
|
+
function formatUsages(usages, name, options = {}) {
|
|
305
|
+
const compact = !!options.compact;
|
|
238
306
|
const defs = usages.filter(u => u.isDefinition);
|
|
239
307
|
const calls = usages.filter(u => u.usageType === 'call');
|
|
240
308
|
const imports = usages.filter(u => u.usageType === 'import');
|
|
@@ -242,7 +310,7 @@ function formatUsages(usages, name) {
|
|
|
242
310
|
|
|
243
311
|
const lines = [];
|
|
244
312
|
lines.push(`Usages of "${name}": ${defs.length} definitions, ${calls.length} calls, ${imports.length} imports, ${refs.length} references`);
|
|
245
|
-
lines.push('═'.repeat(60));
|
|
313
|
+
if (!compact) lines.push('═'.repeat(60));
|
|
246
314
|
|
|
247
315
|
function renderContextLines(usage) {
|
|
248
316
|
if (usage.before && usage.before.length > 0) {
|
|
@@ -261,38 +329,57 @@ function formatUsages(usages, name) {
|
|
|
261
329
|
}
|
|
262
330
|
|
|
263
331
|
if (defs.length > 0) {
|
|
264
|
-
lines.push('
|
|
332
|
+
lines.push(`${compact ? '' : '\n'}DEFINITIONS:`);
|
|
265
333
|
for (const d of defs) {
|
|
266
|
-
|
|
267
|
-
|
|
334
|
+
if (compact) {
|
|
335
|
+
lines.push(` ${d.relativePath}:${d.line || d.startLine}${d.signature ? ' ' + d.signature : ''}`);
|
|
336
|
+
} else {
|
|
337
|
+
lines.push(` ${d.relativePath}:${d.line || d.startLine}`);
|
|
338
|
+
if (d.signature) lines.push(` ${d.signature}`);
|
|
339
|
+
}
|
|
268
340
|
}
|
|
269
341
|
}
|
|
270
342
|
|
|
271
343
|
if (calls.length > 0) {
|
|
272
|
-
lines.push('
|
|
344
|
+
lines.push(`${compact ? '' : '\n'}CALLS:`);
|
|
273
345
|
for (const c of calls) {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
346
|
+
if (compact) {
|
|
347
|
+
const expr = c.content ? c.content.trim().replace(/\s+/g, ' ').slice(0, 100) : '';
|
|
348
|
+
lines.push(` ${c.relativePath}:${c.line}: ${expr}`);
|
|
349
|
+
} else {
|
|
350
|
+
lines.push(` ${c.relativePath}:${c.line}`);
|
|
351
|
+
renderContextLines(c);
|
|
352
|
+
lines.push(` ${c.content.trim()}`);
|
|
353
|
+
renderAfterLines(c);
|
|
354
|
+
}
|
|
278
355
|
}
|
|
279
356
|
}
|
|
280
357
|
|
|
281
358
|
if (imports.length > 0) {
|
|
282
|
-
lines.push('
|
|
359
|
+
lines.push(`${compact ? '' : '\n'}IMPORTS:`);
|
|
283
360
|
for (const i of imports) {
|
|
284
|
-
|
|
285
|
-
|
|
361
|
+
if (compact) {
|
|
362
|
+
const expr = i.content ? i.content.trim().replace(/\s+/g, ' ').slice(0, 100) : '';
|
|
363
|
+
lines.push(` ${i.relativePath}:${i.line}: ${expr}`);
|
|
364
|
+
} else {
|
|
365
|
+
lines.push(` ${i.relativePath}:${i.line}`);
|
|
366
|
+
lines.push(` ${i.content.trim()}`);
|
|
367
|
+
}
|
|
286
368
|
}
|
|
287
369
|
}
|
|
288
370
|
|
|
289
371
|
if (refs.length > 0) {
|
|
290
|
-
lines.push('
|
|
372
|
+
lines.push(`${compact ? '' : '\n'}REFERENCES:`);
|
|
291
373
|
for (const r of refs) {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
374
|
+
if (compact) {
|
|
375
|
+
const expr = r.content ? r.content.trim().replace(/\s+/g, ' ').slice(0, 100) : '';
|
|
376
|
+
lines.push(` ${r.relativePath}:${r.line}: ${expr}`);
|
|
377
|
+
} else {
|
|
378
|
+
lines.push(` ${r.relativePath}:${r.line}`);
|
|
379
|
+
renderContextLines(r);
|
|
380
|
+
lines.push(` ${r.content.trim()}`);
|
|
381
|
+
renderAfterLines(r);
|
|
382
|
+
}
|
|
296
383
|
}
|
|
297
384
|
}
|
|
298
385
|
|
package/core/output/graph.js
CHANGED
|
@@ -105,6 +105,22 @@ function formatExportersJson(exporters, filePath) {
|
|
|
105
105
|
}, null, 2);
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Dedup exported/api symbols. TypeScript overloads emit one symbol per overload
|
|
110
|
+
* declaration with identical name/signature; collapse them to a single entry.
|
|
111
|
+
* Key is "name|signature" (falls back to name|startLine when no signature).
|
|
112
|
+
*/
|
|
113
|
+
function dedupExportSymbols(symbols) {
|
|
114
|
+
if (!Array.isArray(symbols)) return symbols;
|
|
115
|
+
const seen = new Map();
|
|
116
|
+
for (const s of symbols) {
|
|
117
|
+
const sig = s.signature || (s.params !== undefined ? `${s.name}(${s.params})` : '');
|
|
118
|
+
const key = sig ? `${s.name}|${sig}` : `${s.name}|${s.startLine}`;
|
|
119
|
+
if (!seen.has(key)) seen.set(key, s);
|
|
120
|
+
}
|
|
121
|
+
return Array.from(seen.values());
|
|
122
|
+
}
|
|
123
|
+
|
|
108
124
|
/**
|
|
109
125
|
* Format file-exports command output
|
|
110
126
|
*/
|
|
@@ -112,9 +128,10 @@ function formatFileExports(exports, filePath) {
|
|
|
112
128
|
if (exports?.error) return formatFileError(exports, filePath);
|
|
113
129
|
if (exports.length === 0) return `No exports found in ${filePath}`;
|
|
114
130
|
|
|
131
|
+
const deduped = dedupExportSymbols(exports);
|
|
115
132
|
const lines = [];
|
|
116
133
|
lines.push(`Exports from ${filePath}:\n`);
|
|
117
|
-
for (const exp of
|
|
134
|
+
for (const exp of deduped) {
|
|
118
135
|
lines.push(` ${lineRange(exp.startLine, exp.endLine)} ${exp.signature || exp.name}`);
|
|
119
136
|
}
|
|
120
137
|
return lines.join('\n');
|
|
@@ -123,11 +140,13 @@ function formatFileExports(exports, filePath) {
|
|
|
123
140
|
function formatFileExportsJson(result, filePath) {
|
|
124
141
|
if (!result) return JSON.stringify({ found: false });
|
|
125
142
|
// result is an array of exported symbols from index.fileExports()
|
|
143
|
+
const rawExports = Array.isArray(result) ? result : (result.exports || []);
|
|
144
|
+
const exports = dedupExportSymbols(rawExports);
|
|
126
145
|
return JSON.stringify({
|
|
127
146
|
meta: { command: 'fileExports', file: filePath || (Array.isArray(result) ? result[0]?.file : result.file) },
|
|
128
147
|
data: {
|
|
129
148
|
file: filePath || (Array.isArray(result) ? result[0]?.file : result.file),
|
|
130
|
-
exports
|
|
149
|
+
exports,
|
|
131
150
|
},
|
|
132
151
|
}, null, 2);
|
|
133
152
|
}
|
|
@@ -148,7 +167,7 @@ function formatApi(symbols, filePath) {
|
|
|
148
167
|
lines.push('Note: Python requires __all__ for export detection. Use \'toc\' command to see all functions/classes.');
|
|
149
168
|
}
|
|
150
169
|
} else {
|
|
151
|
-
// Group by file
|
|
170
|
+
// Group by file, then dedup overloads within each file group.
|
|
152
171
|
const byFile = new Map();
|
|
153
172
|
for (const sym of symbols) {
|
|
154
173
|
if (!byFile.has(sym.file)) {
|
|
@@ -159,7 +178,8 @@ function formatApi(symbols, filePath) {
|
|
|
159
178
|
|
|
160
179
|
for (const [file, syms] of byFile) {
|
|
161
180
|
lines.push(file);
|
|
162
|
-
|
|
181
|
+
const dedupedSyms = dedupExportSymbols(syms);
|
|
182
|
+
for (const s of dedupedSyms) {
|
|
163
183
|
const sig = s.signature || `${s.type} ${s.name}`;
|
|
164
184
|
lines.push(` ${lineRange(s.startLine, s.endLine)} ${sig}`);
|
|
165
185
|
}
|
|
@@ -174,19 +194,32 @@ function formatApi(symbols, filePath) {
|
|
|
174
194
|
* Format api as JSON
|
|
175
195
|
*/
|
|
176
196
|
function formatApiJson(symbols, filePath) {
|
|
197
|
+
const { formatSymbolHandle } = require('../shared');
|
|
198
|
+
const deduped = dedupExportSymbols(symbols);
|
|
177
199
|
return JSON.stringify({
|
|
200
|
+
meta: { command: 'api', count: deduped.length },
|
|
178
201
|
...(filePath && { file: filePath }),
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
202
|
+
data: {
|
|
203
|
+
exportCount: deduped.length,
|
|
204
|
+
exports: deduped.map(s => {
|
|
205
|
+
const handleSym = { ...s, relativePath: s.relativePath || s.file };
|
|
206
|
+
const handle = formatSymbolHandle(handleSym);
|
|
207
|
+
return {
|
|
208
|
+
name: s.name,
|
|
209
|
+
type: s.type,
|
|
210
|
+
file: s.file,
|
|
211
|
+
startLine: s.startLine,
|
|
212
|
+
endLine: s.endLine,
|
|
213
|
+
...(handle && { handle }),
|
|
214
|
+
...(s.params && { params: s.params }),
|
|
215
|
+
...(s.paramsStructured && { paramsStructured: s.paramsStructured }),
|
|
216
|
+
...(s.paramTypes && { paramTypes: s.paramTypes }),
|
|
217
|
+
...(s.returnType && { returnType: s.returnType }),
|
|
218
|
+
...(s.docstring && { docstring: s.docstring }),
|
|
219
|
+
...(s.signature && { signature: s.signature }),
|
|
220
|
+
};
|
|
221
|
+
}),
|
|
222
|
+
},
|
|
190
223
|
}, null, 2);
|
|
191
224
|
}
|
|
192
225
|
|