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.
Files changed (44) hide show
  1. package/.claude/skills/ucn/SKILL.md +127 -12
  2. package/README.md +152 -156
  3. package/cli/index.js +363 -37
  4. package/core/analysis.js +936 -32
  5. package/core/bridge.js +1095 -0
  6. package/core/brief.js +408 -0
  7. package/core/cache.js +105 -5
  8. package/core/callers.js +72 -18
  9. package/core/check.js +200 -0
  10. package/core/discovery.js +57 -34
  11. package/core/entrypoints.js +638 -4
  12. package/core/execute.js +304 -5
  13. package/core/git-enrich.js +130 -0
  14. package/core/graph.js +24 -2
  15. package/core/output/analysis.js +157 -25
  16. package/core/output/brief.js +100 -0
  17. package/core/output/check.js +79 -0
  18. package/core/output/doctor.js +85 -0
  19. package/core/output/endpoints.js +239 -0
  20. package/core/output/extraction.js +2 -0
  21. package/core/output/find.js +126 -39
  22. package/core/output/graph.js +48 -15
  23. package/core/output/refactoring.js +103 -5
  24. package/core/output/reporting.js +63 -23
  25. package/core/output/search.js +110 -17
  26. package/core/output/shared.js +56 -2
  27. package/core/output.js +4 -0
  28. package/core/parser.js +8 -2
  29. package/core/project.js +39 -3
  30. package/core/registry.js +30 -14
  31. package/core/reporting.js +465 -2
  32. package/core/search.js +130 -52
  33. package/core/shared.js +101 -5
  34. package/core/tracing.js +16 -6
  35. package/core/verify.js +982 -95
  36. package/languages/go.js +91 -6
  37. package/languages/html.js +10 -0
  38. package/languages/java.js +151 -35
  39. package/languages/javascript.js +290 -33
  40. package/languages/python.js +78 -11
  41. package/languages/rust.js +267 -12
  42. package/languages/utils.js +315 -3
  43. package/mcp/server.js +91 -16
  44. 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
- const status = result.mismatches === 0 ? '✓ All calls valid' : '✗ Mismatches found';
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
- lines.push(` ${m.file}:${m.line}`);
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
- lines.push(` ${u.file}:${u.line}`);
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
  };
@@ -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
- ...(results.excludedExported > 0 && { excludedExported: results.excludedExported }),
237
- ...(results.excludedDecorated > 0 && { excludedDecorated: results.excludedDecorated }),
238
- symbols: results.map(item => ({
239
- name: item.name,
240
- type: item.type,
241
- file: item.file,
242
- startLine: item.startLine,
243
- endLine: item.endLine,
244
- ...(item.isExported && { isExported: true }),
245
- ...(item.decorators && item.decorators.length > 0 && { decorators: item.decorators }),
246
- ...(item.annotations && item.annotations.length > 0 && { annotations: item.annotations })
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: ep.name,
310
- file: ep.file,
311
- line: ep.line,
312
- type: ep.type,
313
- framework: ep.framework,
314
- patternId: ep.patternId,
315
- evidence: ep.evidence,
316
- confidence: ep.confidence,
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
  }
@@ -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
- return JSON.stringify({
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
- }, null, 2);
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
- query: name,
243
- count: types.length,
244
- types: types.map(t => ({
245
- name: t.name,
246
- type: t.type,
247
- file: t.relativePath || t.file,
248
- startLine: t.startLine,
249
- endLine: t.endLine,
250
- ...(t.usageCount !== undefined && { usageCount: t.usageCount }),
251
- ...(t.code && { code: t.code })
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: tests.length,
291
- totalMatches: tests.reduce((sum, t) => sum + t.matches.length, 0),
292
- testFiles: tests
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
 
@@ -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
- const params = normalizeParams(fn.params);
95
- sig += `(${params})`;
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
- lines[0] = lines[0].replace(/^(\s*)<script[^>]*>/i, '$1');
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>\s*$/i, '');
291
+ lines[last] = lines[last].replace(/<\/script\s*>/i, '');
286
292
  }
287
293
  return lines;
288
294
  }