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
|
@@ -102,6 +102,22 @@ function formatPlanJson(plan) {
|
|
|
102
102
|
}, null, 2);
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Render the per-site pattern flags as a compact suffix.
|
|
107
|
+
* Returns "" when no flags are set, or " [loop, try, callback, test, awaited]"
|
|
108
|
+
* with only the active flags.
|
|
109
|
+
*/
|
|
110
|
+
function _formatPatternFlags(p) {
|
|
111
|
+
if (!p) return '';
|
|
112
|
+
const flags = [];
|
|
113
|
+
if (p.inLoop) flags.push('loop');
|
|
114
|
+
if (p.inTry) flags.push('try');
|
|
115
|
+
if (p.inCallback) flags.push('callback');
|
|
116
|
+
if (p.inTestCase) flags.push('test');
|
|
117
|
+
if (p.awaited) flags.push('awaited');
|
|
118
|
+
return flags.length ? ` [${flags.join(', ')}]` : '';
|
|
119
|
+
}
|
|
120
|
+
|
|
105
121
|
/**
|
|
106
122
|
* Format verify command output - text
|
|
107
123
|
* Shows call site validation results
|
|
@@ -130,7 +146,19 @@ function formatVerify(result, options = {}) {
|
|
|
130
146
|
lines.push('');
|
|
131
147
|
|
|
132
148
|
// Summary
|
|
133
|
-
|
|
149
|
+
// BUG M1: don't claim "All calls valid" when 0 valid + N uncertain.
|
|
150
|
+
// Status precedence: mismatches > 0 → fail; total === 0 → empty;
|
|
151
|
+
// valid === 0 && uncertain > 0 → all-uncertain; valid > 0 && mismatches === 0 → ok.
|
|
152
|
+
let status;
|
|
153
|
+
if (result.mismatches > 0) {
|
|
154
|
+
status = `✗ ${result.mismatches} mismatch${result.mismatches === 1 ? '' : 'es'}`;
|
|
155
|
+
} else if (result.totalCalls === 0) {
|
|
156
|
+
status = 'ℹ No calls found';
|
|
157
|
+
} else if (result.valid === 0 && result.uncertain > 0) {
|
|
158
|
+
status = '⚠ All calls uncertain (no resolved sites)';
|
|
159
|
+
} else {
|
|
160
|
+
status = '✓ All calls valid';
|
|
161
|
+
}
|
|
134
162
|
lines.push(`STATUS: ${status}`);
|
|
135
163
|
lines.push(` Total calls: ${result.totalCalls}`);
|
|
136
164
|
lines.push(` Valid: ${result.valid}`);
|
|
@@ -140,12 +168,27 @@ function formatVerify(result, options = {}) {
|
|
|
140
168
|
lines.push(` Note: ${result.scopeWarning.hint}`);
|
|
141
169
|
}
|
|
142
170
|
|
|
171
|
+
// Feature A/B: show aggregate patterns counts when any are present.
|
|
172
|
+
const p = result.patterns;
|
|
173
|
+
if (p) {
|
|
174
|
+
const parts = [];
|
|
175
|
+
if (p.inLoop > 0) parts.push(`${p.inLoop} in loop`);
|
|
176
|
+
if (p.inTry > 0) parts.push(`${p.inTry} in try`);
|
|
177
|
+
if (p.inCallback > 0) parts.push(`${p.inCallback} in callback`);
|
|
178
|
+
if (p.inTestCase > 0) parts.push(`${p.inTestCase} in test`);
|
|
179
|
+
if (p.awaitedCalls > 0) parts.push(`${p.awaitedCalls} awaited`);
|
|
180
|
+
if (parts.length > 0) {
|
|
181
|
+
lines.push(` Patterns: ${parts.join(', ')}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
143
185
|
// Show mismatches
|
|
144
186
|
if (result.mismatchDetails.length > 0) {
|
|
145
187
|
lines.push('');
|
|
146
188
|
lines.push('MISMATCHES:');
|
|
147
189
|
for (const m of result.mismatchDetails) {
|
|
148
|
-
|
|
190
|
+
const flags = _formatPatternFlags(m.patterns);
|
|
191
|
+
lines.push(` ${m.file}:${m.line}${flags}`);
|
|
149
192
|
lines.push(` ${m.expression}`);
|
|
150
193
|
lines.push(` Expected ${m.expected}, got ${m.actual}: [${m.args?.join(', ') || ''}]`);
|
|
151
194
|
}
|
|
@@ -156,7 +199,8 @@ function formatVerify(result, options = {}) {
|
|
|
156
199
|
lines.push('');
|
|
157
200
|
lines.push('UNCERTAIN (manual check needed):');
|
|
158
201
|
for (const u of result.uncertainDetails) {
|
|
159
|
-
|
|
202
|
+
const flags = _formatPatternFlags(u.patterns);
|
|
203
|
+
lines.push(` ${u.file}:${u.line}${flags}`);
|
|
160
204
|
lines.push(` ${u.expression}`);
|
|
161
205
|
lines.push(` Reason: ${u.reason}`);
|
|
162
206
|
}
|
|
@@ -187,19 +231,23 @@ function formatVerifyJson(result) {
|
|
|
187
231
|
valid: result.valid,
|
|
188
232
|
mismatches: result.mismatches,
|
|
189
233
|
uncertain: result.uncertain,
|
|
234
|
+
// Feature A/B: surface aggregate patterns and per-site flags.
|
|
235
|
+
patterns: result.patterns,
|
|
190
236
|
mismatchDetails: result.mismatchDetails.map(m => ({
|
|
191
237
|
file: m.file,
|
|
192
238
|
line: m.line,
|
|
193
239
|
expression: m.expression,
|
|
194
240
|
expected: m.expected,
|
|
195
241
|
actual: m.actual,
|
|
196
|
-
args: m.args || []
|
|
242
|
+
args: m.args || [],
|
|
243
|
+
patterns: m.patterns,
|
|
197
244
|
})),
|
|
198
245
|
uncertainDetails: result.uncertainDetails.map(u => ({
|
|
199
246
|
file: u.file,
|
|
200
247
|
line: u.line,
|
|
201
248
|
expression: u.expression,
|
|
202
|
-
reason: u.reason
|
|
249
|
+
reason: u.reason,
|
|
250
|
+
patterns: u.patterns,
|
|
203
251
|
}))
|
|
204
252
|
}, null, 2);
|
|
205
253
|
}
|
|
@@ -283,6 +331,54 @@ function formatStackTraceJson(result) {
|
|
|
283
331
|
}, null, 2);
|
|
284
332
|
}
|
|
285
333
|
|
|
334
|
+
/**
|
|
335
|
+
* Format audit-async command output - text.
|
|
336
|
+
* Lists likely missing-await call sites grouped by file.
|
|
337
|
+
*/
|
|
338
|
+
function formatAuditAsync(result) {
|
|
339
|
+
if (!result) return 'No async audit data.';
|
|
340
|
+
const issues = Array.isArray(result.issues) ? result.issues : [];
|
|
341
|
+
if (issues.length === 0) {
|
|
342
|
+
return 'Async audit: no missing-await issues found.';
|
|
343
|
+
}
|
|
344
|
+
const lines = [];
|
|
345
|
+
lines.push(`Async audit: ${result.totalIssues} likely missing-await call site(s) across ${result.filesAffected} file(s)`);
|
|
346
|
+
lines.push('═'.repeat(60));
|
|
347
|
+
|
|
348
|
+
// Group by file (issues are already sorted by file then line).
|
|
349
|
+
const byFile = new Map();
|
|
350
|
+
for (const issue of issues) {
|
|
351
|
+
if (!byFile.has(issue.file)) byFile.set(issue.file, []);
|
|
352
|
+
byFile.get(issue.file).push(issue);
|
|
353
|
+
}
|
|
354
|
+
for (const [file, fileIssues] of byFile) {
|
|
355
|
+
lines.push('');
|
|
356
|
+
lines.push(`${file} (${fileIssues.length})`);
|
|
357
|
+
for (const issue of fileIssues) {
|
|
358
|
+
const caller = issue.callerName ? ` [${issue.callerName}]` : '';
|
|
359
|
+
lines.push(` :${issue.line}${caller} ${issue.calleeName}() — async, not awaited`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return lines.join('\n');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Format audit-async command output - JSON.
|
|
367
|
+
*/
|
|
368
|
+
function formatAuditAsyncJson(result) {
|
|
369
|
+
if (!result) return JSON.stringify({ issues: [] }, null, 2);
|
|
370
|
+
return JSON.stringify({
|
|
371
|
+
totalIssues: result.totalIssues || 0,
|
|
372
|
+
filesAffected: result.filesAffected || 0,
|
|
373
|
+
issues: (result.issues || []).map(i => ({
|
|
374
|
+
file: i.file,
|
|
375
|
+
line: i.line,
|
|
376
|
+
callerName: i.callerName,
|
|
377
|
+
calleeName: i.calleeName,
|
|
378
|
+
})),
|
|
379
|
+
}, null, 2);
|
|
380
|
+
}
|
|
381
|
+
|
|
286
382
|
module.exports = {
|
|
287
383
|
formatPlan,
|
|
288
384
|
formatPlanJson,
|
|
@@ -290,4 +386,6 @@ module.exports = {
|
|
|
290
386
|
formatVerifyJson,
|
|
291
387
|
formatStackTrace,
|
|
292
388
|
formatStackTraceJson,
|
|
389
|
+
formatAuditAsync,
|
|
390
|
+
formatAuditAsyncJson,
|
|
293
391
|
};
|
package/core/output/reporting.js
CHANGED
|
@@ -150,6 +150,33 @@ function formatStats(stats, options = {}) {
|
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
if (stats.hot) {
|
|
154
|
+
const items = stats.hot.items || [];
|
|
155
|
+
const total = stats.hot.total || items.length;
|
|
156
|
+
lines.push(`\nHottest functions (top ${items.length} of ${total} called):`);
|
|
157
|
+
if (items.length === 0) {
|
|
158
|
+
lines.push(' (no inbound calls detected)');
|
|
159
|
+
} else {
|
|
160
|
+
for (const fn of items) {
|
|
161
|
+
const loc = `${fn.file}:${fn.startLine}`;
|
|
162
|
+
lines.push(` ${String(fn.callCount).padStart(5)} calls ${fn.name} (${loc})`);
|
|
163
|
+
// MEDIUM-6: when the same name has multiple definitions across
|
|
164
|
+
// files (e.g. test helpers vs. test fixtures both named `tmp`),
|
|
165
|
+
// list the additional locations indented so the user knows
|
|
166
|
+
// the count covers ambiguous resolution.
|
|
167
|
+
if (Array.isArray(fn.locations) && fn.locations.length > 1) {
|
|
168
|
+
for (let i = 1; i < fn.locations.length; i++) {
|
|
169
|
+
const l = fn.locations[i];
|
|
170
|
+
lines.push(` ↳ also defined at ${l.file}:${l.startLine}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (total > items.length) {
|
|
175
|
+
lines.push(` ... ${total - items.length} more (use --top=N to show more)`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
153
180
|
return lines.join('\n');
|
|
154
181
|
}
|
|
155
182
|
|
|
@@ -231,20 +258,29 @@ function formatDeadcode(results, options = {}) {
|
|
|
231
258
|
* Format deadcode command output - JSON
|
|
232
259
|
*/
|
|
233
260
|
function formatDeadcodeJson(results) {
|
|
261
|
+
const { formatSymbolHandle } = require('../shared');
|
|
234
262
|
return JSON.stringify({
|
|
235
|
-
count: results.length,
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
263
|
+
meta: { command: 'deadcode', count: results.length },
|
|
264
|
+
data: {
|
|
265
|
+
count: results.length,
|
|
266
|
+
...(results.excludedExported > 0 && { excludedExported: results.excludedExported }),
|
|
267
|
+
...(results.excludedDecorated > 0 && { excludedDecorated: results.excludedDecorated }),
|
|
268
|
+
symbols: results.map(item => {
|
|
269
|
+
const handleSym = { ...item, relativePath: item.relativePath || item.file };
|
|
270
|
+
const handle = formatSymbolHandle(handleSym);
|
|
271
|
+
return {
|
|
272
|
+
name: item.name,
|
|
273
|
+
type: item.type,
|
|
274
|
+
file: item.file,
|
|
275
|
+
startLine: item.startLine,
|
|
276
|
+
endLine: item.endLine,
|
|
277
|
+
...(handle && { handle }),
|
|
278
|
+
...(item.isExported && { isExported: true }),
|
|
279
|
+
...(item.decorators && item.decorators.length > 0 && { decorators: item.decorators }),
|
|
280
|
+
...(item.annotations && item.annotations.length > 0 && { annotations: item.annotations })
|
|
281
|
+
};
|
|
282
|
+
}),
|
|
283
|
+
},
|
|
248
284
|
}, null, 2);
|
|
249
285
|
}
|
|
250
286
|
|
|
@@ -305,16 +341,20 @@ function formatEntrypointsJson(results) {
|
|
|
305
341
|
return JSON.stringify({
|
|
306
342
|
meta: { total: results.length },
|
|
307
343
|
data: {
|
|
308
|
-
entrypoints: results.map(ep =>
|
|
309
|
-
name
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
344
|
+
entrypoints: results.map(ep => {
|
|
345
|
+
const handle = ep.line && ep.name ? `${ep.file}:${ep.line}:${ep.name}` : null;
|
|
346
|
+
return {
|
|
347
|
+
name: ep.name,
|
|
348
|
+
file: ep.file,
|
|
349
|
+
line: ep.line,
|
|
350
|
+
...(handle && { handle }),
|
|
351
|
+
type: ep.type,
|
|
352
|
+
framework: ep.framework,
|
|
353
|
+
patternId: ep.patternId,
|
|
354
|
+
evidence: ep.evidence,
|
|
355
|
+
confidence: ep.confidence,
|
|
356
|
+
};
|
|
357
|
+
}),
|
|
318
358
|
}
|
|
319
359
|
}, null, 2);
|
|
320
360
|
}
|
package/core/output/search.js
CHANGED
|
@@ -151,8 +151,20 @@ function formatStructuralSearchJson(result) {
|
|
|
151
151
|
* Format example result as text
|
|
152
152
|
*/
|
|
153
153
|
function formatExample(result, name) {
|
|
154
|
+
// MEDIUM-8: when only test-file callers exist and the user didn't ask
|
|
155
|
+
// for them, surface that fact explicitly instead of saying nothing was
|
|
156
|
+
// found.
|
|
157
|
+
if (result && !result.best && result.excludedTestCalls > 0) {
|
|
158
|
+
const n = result.excludedTestCalls;
|
|
159
|
+
return `No call examples found for "${name}" (excluded ${n} test-file usage${n === 1 ? '' : 's'} — pass --include-tests to include them)`;
|
|
160
|
+
}
|
|
154
161
|
if (!result || !result.best) return `No call examples found for "${name}"`;
|
|
155
162
|
|
|
163
|
+
// Diverse mode: render one block per cluster representative.
|
|
164
|
+
if (result.clusters && result.clusters.length > 0) {
|
|
165
|
+
return formatExampleDiverse(result, name);
|
|
166
|
+
}
|
|
167
|
+
|
|
156
168
|
const best = result.best;
|
|
157
169
|
const lines = [];
|
|
158
170
|
lines.push(`Best example of "${name}":`);
|
|
@@ -183,6 +195,55 @@ function formatExample(result, name) {
|
|
|
183
195
|
return lines.join('\n');
|
|
184
196
|
}
|
|
185
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Render `example --diverse` output: one representative per call-shape cluster.
|
|
200
|
+
* Each block shows the shape signature, cluster size, and the representative
|
|
201
|
+
* with code context — so an agent can see "calls fall into N distinct shapes,
|
|
202
|
+
* here's an example of each".
|
|
203
|
+
*/
|
|
204
|
+
function formatExampleDiverse(result, name) {
|
|
205
|
+
const lines = [];
|
|
206
|
+
const total = result.totalClusters || result.clusters.length;
|
|
207
|
+
lines.push(`Diverse examples of "${name}" — ${result.clusters.length} of ${total} cluster(s), ${result.totalCalls} total calls:`);
|
|
208
|
+
lines.push('═'.repeat(60));
|
|
209
|
+
|
|
210
|
+
for (let i = 0; i < result.clusters.length; i++) {
|
|
211
|
+
const c = result.clusters[i];
|
|
212
|
+
const rep = c.representative;
|
|
213
|
+
const shape = c.argKinds == null
|
|
214
|
+
? 'unknown shape'
|
|
215
|
+
: c.argKinds.length === 0
|
|
216
|
+
? 'no arguments'
|
|
217
|
+
: `args: (${c.argKinds.join(', ')})`;
|
|
218
|
+
|
|
219
|
+
lines.push('');
|
|
220
|
+
lines.push(`[${i + 1}] ${shape} — ${c.count} call${c.count === 1 ? '' : 's'} in this cluster`);
|
|
221
|
+
if (!rep) continue;
|
|
222
|
+
lines.push(` ${rep.relativePath || rep.file}:${rep.line}`);
|
|
223
|
+
|
|
224
|
+
if (rep.before) {
|
|
225
|
+
for (let j = 0; j < rep.before.length; j++) {
|
|
226
|
+
const ln = rep.line - rep.before.length + j;
|
|
227
|
+
lines.push(` ${ln.toString().padStart(4)}| ${rep.before[j]}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
lines.push(` ${rep.line.toString().padStart(4)}| ${rep.content} <--`);
|
|
231
|
+
if (rep.after) {
|
|
232
|
+
for (let j = 0; j < rep.after.length; j++) {
|
|
233
|
+
const ln = rep.line + j + 1;
|
|
234
|
+
lines.push(` ${ln.toString().padStart(4)}| ${rep.after[j]}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (total > result.clusters.length) {
|
|
240
|
+
lines.push('');
|
|
241
|
+
lines.push(`... ${total - result.clusters.length} more cluster(s) (use --top=N to show more)`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return lines.join('\n');
|
|
245
|
+
}
|
|
246
|
+
|
|
186
247
|
/**
|
|
187
248
|
* Format example command output - JSON
|
|
188
249
|
*/
|
|
@@ -192,7 +253,7 @@ function formatExampleJson(result, name) {
|
|
|
192
253
|
}
|
|
193
254
|
|
|
194
255
|
const best = result.best;
|
|
195
|
-
|
|
256
|
+
const env = {
|
|
196
257
|
found: true,
|
|
197
258
|
query: name,
|
|
198
259
|
totalCalls: result.totalCalls,
|
|
@@ -205,7 +266,29 @@ function formatExampleJson(result, name) {
|
|
|
205
266
|
...(best.before && best.before.length > 0 && { before: best.before }),
|
|
206
267
|
...(best.after && best.after.length > 0 && { after: best.after })
|
|
207
268
|
}
|
|
208
|
-
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
if (result.clusters && result.clusters.length > 0) {
|
|
272
|
+
env.totalClusters = result.totalClusters;
|
|
273
|
+
env.clusters = result.clusters.map(c => ({
|
|
274
|
+
shapeKey: c.shapeKey,
|
|
275
|
+
argCount: c.argCount,
|
|
276
|
+
argKinds: c.argKinds,
|
|
277
|
+
count: c.count,
|
|
278
|
+
representative: c.representative ? {
|
|
279
|
+
file: c.representative.relativePath || c.representative.file,
|
|
280
|
+
line: c.representative.line,
|
|
281
|
+
content: c.representative.content,
|
|
282
|
+
score: c.representative.score,
|
|
283
|
+
reasons: c.representative.reasons || [],
|
|
284
|
+
...(c.representative._argTexts && { argTexts: c.representative._argTexts }),
|
|
285
|
+
...(c.representative.before && c.representative.before.length > 0 && { before: c.representative.before }),
|
|
286
|
+
...(c.representative.after && c.representative.after.length > 0 && { after: c.representative.after }),
|
|
287
|
+
} : null,
|
|
288
|
+
}));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return JSON.stringify(env, null, 2);
|
|
209
292
|
}
|
|
210
293
|
|
|
211
294
|
/**
|
|
@@ -238,18 +321,27 @@ function formatTypedef(types, name) {
|
|
|
238
321
|
* Format typedef as JSON
|
|
239
322
|
*/
|
|
240
323
|
function formatTypedefJson(types, name) {
|
|
324
|
+
const { formatSymbolHandle } = require('../shared');
|
|
241
325
|
return JSON.stringify({
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
326
|
+
meta: { command: 'typedef', count: types.length },
|
|
327
|
+
data: {
|
|
328
|
+
query: name,
|
|
329
|
+
count: types.length,
|
|
330
|
+
types: types.map(t => {
|
|
331
|
+
const handle = formatSymbolHandle(t);
|
|
332
|
+
return {
|
|
333
|
+
name: t.name,
|
|
334
|
+
type: t.type,
|
|
335
|
+
file: t.relativePath || t.file,
|
|
336
|
+
startLine: t.startLine,
|
|
337
|
+
endLine: t.endLine,
|
|
338
|
+
...(handle && { handle }),
|
|
339
|
+
...(t.docstring && { docstring: t.docstring }),
|
|
340
|
+
...(t.usageCount !== undefined && { usageCount: t.usageCount }),
|
|
341
|
+
...(t.code && { code: t.code }),
|
|
342
|
+
};
|
|
343
|
+
}),
|
|
344
|
+
},
|
|
253
345
|
}, null, 2);
|
|
254
346
|
}
|
|
255
347
|
|
|
@@ -259,7 +351,7 @@ function formatTypedefJson(types, name) {
|
|
|
259
351
|
function formatTests(tests, name) {
|
|
260
352
|
const lines = [`Tests for "${name}":\n`];
|
|
261
353
|
|
|
262
|
-
if (tests.length === 0) {
|
|
354
|
+
if (!tests || !Array.isArray(tests) || tests.length === 0) {
|
|
263
355
|
lines.push(' (no tests found)');
|
|
264
356
|
} else {
|
|
265
357
|
const totalMatches = tests.reduce((sum, t) => sum + t.matches.length, 0);
|
|
@@ -285,11 +377,12 @@ function formatTests(tests, name) {
|
|
|
285
377
|
* Format tests as JSON
|
|
286
378
|
*/
|
|
287
379
|
function formatTestsJson(tests, name) {
|
|
380
|
+
const safe = Array.isArray(tests) ? tests : [];
|
|
288
381
|
return JSON.stringify({
|
|
289
382
|
query: name,
|
|
290
|
-
testFileCount:
|
|
291
|
-
totalMatches:
|
|
292
|
-
testFiles:
|
|
383
|
+
testFileCount: safe.length,
|
|
384
|
+
totalMatches: safe.reduce((sum, t) => sum + (t.matches?.length || 0), 0),
|
|
385
|
+
testFiles: safe
|
|
293
386
|
}, null, 2);
|
|
294
387
|
}
|
|
295
388
|
|
package/core/output/shared.js
CHANGED
|
@@ -91,8 +91,17 @@ function formatFunctionSignature(fn) {
|
|
|
91
91
|
// Name + generics + params (concatenated without spaces)
|
|
92
92
|
let sig = fn.name;
|
|
93
93
|
if (fn.generics) sig += fn.generics;
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
// If paramsStructured + paramTypes are available, render typed params
|
|
95
|
+
const typed = renderTypedParams(fn);
|
|
96
|
+
// When paramsStructured is an empty array, the function has zero params —
|
|
97
|
+
// render `()` rather than the legacy `(...)` placeholder.
|
|
98
|
+
const noParams = Array.isArray(fn.paramsStructured) && fn.paramsStructured.length === 0;
|
|
99
|
+
let paramText;
|
|
100
|
+
if (typed != null) paramText = typed;
|
|
101
|
+
else if (noParams) paramText = '';
|
|
102
|
+
else if (fn.params != null && fn.params !== '...') paramText = normalizeParams(fn.params);
|
|
103
|
+
else paramText = '...';
|
|
104
|
+
sig += `(${paramText})`;
|
|
96
105
|
|
|
97
106
|
// Return type
|
|
98
107
|
if (fn.returnType) sig += `: ${fn.returnType}`;
|
|
@@ -106,6 +115,29 @@ function formatFunctionSignature(fn) {
|
|
|
106
115
|
return sig;
|
|
107
116
|
}
|
|
108
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Render parameters with type annotations when available.
|
|
120
|
+
* Returns null if not enough info — caller falls back to raw params string.
|
|
121
|
+
*/
|
|
122
|
+
function renderTypedParams(fn) {
|
|
123
|
+
const ps = fn.paramsStructured;
|
|
124
|
+
if (!Array.isArray(ps) || ps.length === 0) return null;
|
|
125
|
+
const paramTypes = fn.paramTypes;
|
|
126
|
+
const hasStructuredTypes = ps.some(p => p && p.type);
|
|
127
|
+
const hasMappedTypes = paramTypes && Object.keys(paramTypes).length > 0;
|
|
128
|
+
if (!hasStructuredTypes && !hasMappedTypes) return null;
|
|
129
|
+
const parts = ps.map(p => {
|
|
130
|
+
if (!p || !p.name) return '';
|
|
131
|
+
let s = p.rest ? `...${p.name.replace(/^\.\.\./, '')}` : p.name;
|
|
132
|
+
const t = p.type || (paramTypes && paramTypes[p.name]);
|
|
133
|
+
if (t) s += `: ${t}`;
|
|
134
|
+
if (p.optional && !p.rest && p.default == null) s += '?';
|
|
135
|
+
if (p.default != null) s += ` = ${p.default}`;
|
|
136
|
+
return s;
|
|
137
|
+
});
|
|
138
|
+
return parts.filter(Boolean).join(', ');
|
|
139
|
+
}
|
|
140
|
+
|
|
109
141
|
/**
|
|
110
142
|
* Format class/type signature for TOC display
|
|
111
143
|
*/
|
|
@@ -254,6 +286,26 @@ function computeConfidence(symbol) {
|
|
|
254
286
|
return { level, reasons };
|
|
255
287
|
}
|
|
256
288
|
|
|
289
|
+
/**
|
|
290
|
+
* Render a single human-readable line for git enrichment data.
|
|
291
|
+
* Format: `Last modified: <ISO> by <author> · <N> commits in last 30d`
|
|
292
|
+
*
|
|
293
|
+
* Returns null when input is missing or unavailable so callers can decide
|
|
294
|
+
* whether to push it. Used by `about` and `brief` formatters.
|
|
295
|
+
*/
|
|
296
|
+
function formatGitLine(git) {
|
|
297
|
+
if (!git || !git.available) return null;
|
|
298
|
+
const parts = [];
|
|
299
|
+
if (git.lastModified) parts.push(`Last modified: ${git.lastModified}`);
|
|
300
|
+
if (git.author) parts.push(`by ${git.author}`);
|
|
301
|
+
let line = parts.join(' ');
|
|
302
|
+
if (git.recentChanges != null) {
|
|
303
|
+
const tail = `${git.recentChanges} commit${git.recentChanges === 1 ? '' : 's'} in last 30d`;
|
|
304
|
+
line = line ? `${line} · ${tail}` : tail;
|
|
305
|
+
}
|
|
306
|
+
return line;
|
|
307
|
+
}
|
|
308
|
+
|
|
257
309
|
module.exports = {
|
|
258
310
|
dynamicImportsNote,
|
|
259
311
|
formatFileError,
|
|
@@ -262,10 +314,12 @@ module.exports = {
|
|
|
262
314
|
lineRange,
|
|
263
315
|
lineLoc,
|
|
264
316
|
formatFunctionSignature,
|
|
317
|
+
renderTypedParams,
|
|
265
318
|
formatClassSignature,
|
|
266
319
|
formatMemberSignature,
|
|
267
320
|
formatLineRanges,
|
|
268
321
|
detectDoubleEscaping,
|
|
269
322
|
countNestedGenerics,
|
|
270
323
|
computeConfidence,
|
|
324
|
+
formatGitLine,
|
|
271
325
|
};
|
package/core/output.js
CHANGED
|
@@ -19,4 +19,8 @@ module.exports = {
|
|
|
19
19
|
...require('./output/extraction'),
|
|
20
20
|
...require('./output/reporting'),
|
|
21
21
|
...require('./output/refactoring'),
|
|
22
|
+
...require('./output/brief'),
|
|
23
|
+
...require('./output/doctor'),
|
|
24
|
+
...require('./output/check'),
|
|
25
|
+
...require('./output/endpoints'),
|
|
22
26
|
};
|
package/core/parser.js
CHANGED
|
@@ -274,15 +274,21 @@ function getExportedSymbols(result) {
|
|
|
274
274
|
/**
|
|
275
275
|
* Strip <script> and </script> tags from extracted code lines for HTML files.
|
|
276
276
|
* Only affects the first and last lines when they contain script tags alongside JS code.
|
|
277
|
+
* Handles inline cases like `<p>foo</p><script>code</script><p>bar</p>` by stripping
|
|
278
|
+
* the opening/closing tags wherever they appear on the line, not just at line edges.
|
|
277
279
|
* @param {string[]} lines - Extracted lines
|
|
278
280
|
* @param {string} language - Language name
|
|
279
281
|
* @returns {string[]} Cleaned lines (same array mutated)
|
|
280
282
|
*/
|
|
281
283
|
function cleanHtmlScriptTags(lines, language) {
|
|
282
284
|
if (language === 'html' && lines.length > 0) {
|
|
283
|
-
|
|
285
|
+
// Strip opening <script ...> tag wherever it appears on the first line
|
|
286
|
+
// (may be preceded by other HTML on the same line, e.g. <p>x</p><script>...).
|
|
287
|
+
lines[0] = lines[0].replace(/<script\b[^>]*>/i, '');
|
|
288
|
+
// Strip closing </script> tag wherever it appears on the last line
|
|
289
|
+
// (may be followed by other HTML on the same line, e.g. ...</script><p>x</p>).
|
|
284
290
|
const last = lines.length - 1;
|
|
285
|
-
lines[last] = lines[last].replace(/<\/script
|
|
291
|
+
lines[last] = lines[last].replace(/<\/script\s*>/i, '');
|
|
286
292
|
}
|
|
287
293
|
return lines;
|
|
288
294
|
}
|