ucn 3.8.12 → 3.8.14

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 +3 -1
  2. package/.github/workflows/ci.yml +15 -3
  3. package/.github/workflows/publish.yml +20 -8
  4. package/README.md +1 -0
  5. package/cli/index.js +165 -246
  6. package/core/analysis.js +1400 -0
  7. package/core/build-worker.js +194 -0
  8. package/core/cache.js +105 -7
  9. package/core/callers.js +194 -64
  10. package/core/deadcode.js +22 -66
  11. package/core/discovery.js +9 -54
  12. package/core/execute.js +139 -54
  13. package/core/graph.js +615 -0
  14. package/core/output/analysis-ext.js +271 -0
  15. package/core/output/analysis.js +491 -0
  16. package/core/output/extraction.js +188 -0
  17. package/core/output/find.js +355 -0
  18. package/core/output/graph.js +399 -0
  19. package/core/output/refactoring.js +293 -0
  20. package/core/output/reporting.js +331 -0
  21. package/core/output/search.js +307 -0
  22. package/core/output/shared.js +271 -0
  23. package/core/output/tracing.js +416 -0
  24. package/core/output.js +15 -3293
  25. package/core/parallel-build.js +165 -0
  26. package/core/project.js +299 -3633
  27. package/core/registry.js +59 -0
  28. package/core/reporting.js +258 -0
  29. package/core/search.js +890 -0
  30. package/core/stacktrace.js +1 -1
  31. package/core/tracing.js +631 -0
  32. package/core/verify.js +10 -13
  33. package/eslint.config.js +43 -0
  34. package/jsconfig.json +10 -0
  35. package/languages/go.js +21 -2
  36. package/languages/html.js +8 -0
  37. package/languages/index.js +102 -40
  38. package/languages/java.js +13 -0
  39. package/languages/javascript.js +17 -1
  40. package/languages/python.js +14 -0
  41. package/languages/rust.js +13 -0
  42. package/languages/utils.js +1 -1
  43. package/mcp/server.js +45 -28
  44. package/package.json +8 -3
package/core/registry.js CHANGED
@@ -84,8 +84,65 @@ const PARAM_MAP = {
84
84
  top_level: 'topLevel',
85
85
  max_files: 'maxFiles',
86
86
  max_chars: 'maxChars',
87
+ follow_symlinks: 'followSymlinks',
87
88
  };
88
89
 
90
+ // ============================================================================
91
+ // FLAG APPLICABILITY MATRIX
92
+ // ============================================================================
93
+
94
+ // Per-command list of accepted flag names (camelCase). Source of truth for help text,
95
+ // MCP schema validation, and architecture guards.
96
+ // file* = file is the command subject (required), not a filter pattern.
97
+ const FLAG_APPLICABILITY = {
98
+ // Understanding code
99
+ about: ['file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'top', 'all', 'withTypes', 'minConfidence', 'showConfidence'],
100
+ context: ['file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'minConfidence', 'showConfidence'],
101
+ impact: ['file', 'exclude', 'className', 'top'],
102
+ blast: ['file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'depth', 'all', 'minConfidence'],
103
+ reverseTrace: ['file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'depth', 'all', 'minConfidence'],
104
+ smart: ['file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'withTypes', 'minConfidence'],
105
+ trace: ['file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'depth', 'all', 'minConfidence'],
106
+ example: ['file', 'className'],
107
+ related: ['file', 'className', 'top', 'all'],
108
+ // Finding code
109
+ find: ['file', 'exclude', 'className', 'includeTests', 'top', 'limit', 'exact', 'in', 'all'],
110
+ usages: ['file', 'exclude', 'className', 'includeTests', 'limit', 'codeOnly', 'context', 'in'],
111
+ toc: ['file', 'exclude', 'top', 'limit', 'all', 'detailed', 'topLevel', 'in'],
112
+ search: ['file', 'exclude', 'includeTests', 'top', 'limit', 'codeOnly', 'caseSensitive', 'context', 'regex', 'in', 'type', 'param', 'receiver', 'returns', 'decorator', 'exported', 'unused'],
113
+ tests: ['className', 'callsOnly'],
114
+ affectedTests:['file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'depth', 'minConfidence'],
115
+ deadcode: ['file', 'exclude', 'includeTests', 'includeExported', 'includeDecorated', 'limit', 'in'],
116
+ entrypoints: ['file', 'exclude', 'includeTests', 'limit', 'type', 'framework'],
117
+ // Extracting code
118
+ fn: ['file', 'className', 'all'],
119
+ class: ['file', 'className', 'all', 'maxLines'],
120
+ lines: ['file', 'range'],
121
+ expand: [],
122
+ // File dependencies
123
+ imports: ['file'],
124
+ exporters: ['file'],
125
+ fileExports: ['file'],
126
+ graph: ['file', 'depth', 'direction'],
127
+ circularDeps: ['file', 'exclude'],
128
+ // Refactoring
129
+ verify: ['file', 'className'],
130
+ plan: ['file', 'className', 'addParam', 'removeParam', 'renameTo', 'defaultValue'],
131
+ diffImpact: ['file', 'limit', 'base', 'staged', 'all'],
132
+ // Other
133
+ typedef: ['file', 'className', 'exact'],
134
+ stacktrace: ['stack'],
135
+ api: ['file', 'limit'],
136
+ stats: ['functions', 'top'],
137
+ };
138
+
139
+ // Commands whose output is project-wide — truncation means you need a filter, not more text.
140
+ // Used by MCP server for tighter default output limits.
141
+ const BROAD_COMMANDS = new Set([
142
+ 'toc', 'entrypoints', 'diffImpact', 'affectedTests',
143
+ 'deadcode', 'usages', 'reverseTrace', 'circularDeps',
144
+ ]);
145
+
89
146
  // ============================================================================
90
147
  // HELPERS
91
148
  // ============================================================================
@@ -172,6 +229,8 @@ module.exports = {
172
229
  CLI_ALIASES,
173
230
  MCP_ALIASES,
174
231
  PARAM_MAP,
232
+ FLAG_APPLICABILITY,
233
+ BROAD_COMMANDS,
175
234
  resolveCommand,
176
235
  normalizeParams,
177
236
  getCliCommandSet,
@@ -0,0 +1,258 @@
1
+ /**
2
+ * core/reporting.js — Project statistics and table of contents
3
+ *
4
+ * Extracted from project.js. All functions take an `index` (ProjectIndex)
5
+ * as the first argument instead of using `this`.
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const { isTestFile } = require('./discovery');
13
+
14
+ /**
15
+ * Get project statistics: file counts, symbol counts, LOC, language breakdown.
16
+ *
17
+ * @param {object} index - ProjectIndex instance
18
+ * @param {object} options - { functions }
19
+ * @returns {object}
20
+ */
21
+ function getStats(index, options = {}) {
22
+ // Count total symbols (not just unique names)
23
+ let totalSymbols = 0;
24
+ for (const [name, symbols] of index.symbols) {
25
+ totalSymbols += symbols.length;
26
+ }
27
+
28
+ const stats = {
29
+ root: index.root,
30
+ files: index.files.size,
31
+ symbols: totalSymbols, // Total symbol count, not unique names
32
+ buildTime: index.buildTime,
33
+ byLanguage: {},
34
+ byType: {},
35
+ ...(index.truncated && { truncated: index.truncated })
36
+ };
37
+
38
+ for (const [filePath, fileEntry] of index.files) {
39
+ const lang = fileEntry.language;
40
+ if (!stats.byLanguage[lang]) {
41
+ stats.byLanguage[lang] = { files: 0, lines: 0, symbols: 0 };
42
+ }
43
+ stats.byLanguage[lang].files++;
44
+ stats.byLanguage[lang].lines += fileEntry.lines;
45
+ stats.byLanguage[lang].symbols += fileEntry.symbols.length;
46
+ }
47
+
48
+ for (const [name, symbols] of index.symbols) {
49
+ for (const sym of symbols) {
50
+ if (!Object.hasOwn(stats.byType, sym.type)) {
51
+ stats.byType[sym.type] = 0;
52
+ }
53
+ stats.byType[sym.type]++;
54
+ }
55
+ }
56
+
57
+ // Surface build warnings (parse failures, skipped files)
58
+ if (index.failedFiles && index.failedFiles.size > 0) {
59
+ stats.warnings = {
60
+ failedFiles: [...index.failedFiles].map(f => path.relative(index.root, f)),
61
+ count: index.failedFiles.size
62
+ };
63
+ }
64
+
65
+ // Per-function line counts for complexity audits
66
+ if (options.functions) {
67
+ const functions = [];
68
+ for (const [name, symbols] of index.symbols) {
69
+ for (const sym of symbols) {
70
+ if (sym.type === 'function' || sym.type === 'method' || sym.type === 'static' ||
71
+ sym.type === 'constructor' || sym.type === 'public' || sym.type === 'abstract' ||
72
+ sym.type === 'classmethod') {
73
+ const lineCount = sym.endLine - sym.startLine + 1;
74
+ const relativePath = sym.relativePath || (sym.file ? path.relative(index.root, sym.file) : '');
75
+ functions.push({
76
+ name: sym.className ? `${sym.className}.${sym.name}` : sym.name,
77
+ file: relativePath,
78
+ startLine: sym.startLine,
79
+ lines: lineCount
80
+ });
81
+ }
82
+ }
83
+ }
84
+ functions.sort((a, b) => b.lines - a.lines);
85
+ stats.functions = functions;
86
+ }
87
+
88
+ return stats;
89
+ }
90
+
91
+ /**
92
+ * Get table of contents for all files in the project.
93
+ *
94
+ * @param {object} index - ProjectIndex instance
95
+ * @param {object} options - { file, exclude, in, detailed, topLevel, all, top }
96
+ * @returns {object}
97
+ */
98
+ function getToc(index, options = {}) {
99
+ const files = [];
100
+ let totalFunctions = 0;
101
+ let totalClasses = 0;
102
+ let totalState = 0;
103
+ let totalLines = 0;
104
+ let totalDynamic = 0;
105
+ let totalTests = 0;
106
+
107
+ // When file= is specified, scope to matching files only
108
+ let fileFilter = null;
109
+ if (options.file) {
110
+ const resolved = index.findFile(options.file);
111
+ if (resolved) {
112
+ fileFilter = new Set([resolved]);
113
+ } else {
114
+ // Try substring match for partial paths
115
+ const matching = [];
116
+ for (const fp of index.files.keys()) {
117
+ const rp = path.relative(index.root, fp);
118
+ if (rp.includes(options.file) || fp.includes(options.file)) {
119
+ matching.push(fp);
120
+ }
121
+ }
122
+ if (matching.length > 0) {
123
+ fileFilter = new Set(matching);
124
+ } else {
125
+ return {
126
+ meta: { complete: true, skipped: 0, dynamicImports: 0, uncertain: 0 },
127
+ totals: { files: 0, lines: 0, functions: 0, classes: 0, state: 0, testFiles: 0 },
128
+ summary: { topFunctionFiles: [], topLineFiles: [], entryFiles: [] },
129
+ files: [],
130
+ hiddenFiles: 0,
131
+ error: `File not found in project: ${options.file}`
132
+ };
133
+ }
134
+ }
135
+ }
136
+
137
+ for (const [filePath, fileEntry] of index.files) {
138
+ if (fileFilter && !fileFilter.has(filePath)) continue;
139
+ if (options.exclude && options.exclude.length > 0) {
140
+ if (!index.matchesFilters(fileEntry.relativePath, { exclude: options.exclude })) continue;
141
+ }
142
+ if (options.in) {
143
+ if (!index.matchesFilters(fileEntry.relativePath, { in: options.in })) continue;
144
+ }
145
+ let functions = fileEntry.symbols.filter(s =>
146
+ s.type === 'function' || s.type === 'method' || s.type === 'static' ||
147
+ s.type === 'constructor' || s.type === 'public' || s.type === 'abstract' ||
148
+ s.type === 'classmethod'
149
+ );
150
+ const classes = fileEntry.symbols.filter(s =>
151
+ ['class', 'interface', 'type', 'enum', 'struct', 'trait', 'impl', 'record', 'namespace'].includes(s.type)
152
+ );
153
+ const state = fileEntry.symbols.filter(s => s.type === 'state');
154
+
155
+ if (options.topLevel) {
156
+ functions = functions.filter(fn => !fn.isNested && (!fn.indent || fn.indent === 0));
157
+ }
158
+
159
+ totalFunctions += functions.length;
160
+ totalClasses += classes.length;
161
+ totalState += state.length;
162
+ totalLines += fileEntry.lines;
163
+ totalDynamic += fileEntry.dynamicImports || 0;
164
+ if (isTestFile(fileEntry.relativePath, fileEntry.language)) totalTests += 1;
165
+
166
+ const entry = {
167
+ file: fileEntry.relativePath,
168
+ language: fileEntry.language,
169
+ lines: fileEntry.lines,
170
+ functions: functions.length,
171
+ classes: classes.length,
172
+ state: state.length
173
+ };
174
+
175
+ if (options.detailed) {
176
+ entry.symbols = { functions, classes, state };
177
+ }
178
+
179
+ files.push(entry);
180
+ }
181
+
182
+ // Hints: top files by function count and lines
183
+ const hintLimit = options.all ? Infinity : 3;
184
+ const topFunctionFiles = [...files]
185
+ .sort((a, b) => b.functions - a.functions || b.lines - a.lines)
186
+ .filter(f => f.functions > 0)
187
+ .slice(0, hintLimit)
188
+ .map(f => ({ file: f.file, functions: f.functions }));
189
+
190
+ const topLineFiles = [...files]
191
+ .sort((a, b) => b.lines - a.lines)
192
+ .slice(0, hintLimit)
193
+ .map(f => ({ file: f.file, lines: f.lines }));
194
+
195
+ // Entry point candidates
196
+ const entryPattern = /(main|index|server|app)\.(js|jsx|ts|tsx|py|go|rs|java)$/i;
197
+ const entryFiles = files
198
+ .filter(f => entryPattern.test(f.file))
199
+ .slice(0, options.all ? Infinity : 5)
200
+ .map(f => f.file);
201
+
202
+ // Also detect entry points from package.json main/exports fields
203
+ const pkgJsonPath = path.join(index.root, 'package.json');
204
+ try {
205
+ const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
206
+ const mainField = pkgJson.main || pkgJson.module;
207
+ if (mainField) {
208
+ const mainFile = path.relative(index.root, path.resolve(index.root, mainField));
209
+ if (files.some(f => f.file === mainFile) && !entryFiles.includes(mainFile)) {
210
+ entryFiles.unshift(mainFile);
211
+ }
212
+ }
213
+ } catch {
214
+ // No package.json or invalid JSON — skip
215
+ }
216
+
217
+ // Apply top limit for detailed mode to avoid massive output
218
+ const top = options.top > 0 ? options.top : (options.detailed && !options.all ? 50 : Infinity);
219
+ let hiddenFiles = 0;
220
+ let displayFiles = files;
221
+ if (top < files.length) {
222
+ hiddenFiles = files.length - top;
223
+ displayFiles = files.slice(0, top);
224
+ }
225
+
226
+ // Count files with no symbols (generated/empty files)
227
+ const emptyFiles = files.filter(f => f.functions === 0 && f.classes === 0 && f.state === 0).length;
228
+
229
+ return {
230
+ meta: {
231
+ complete: totalDynamic === 0,
232
+ skipped: 0,
233
+ dynamicImports: totalDynamic,
234
+ uncertain: 0,
235
+ projectLanguage: index._getPredominantLanguage(),
236
+ ...(fileFilter && { filteredBy: options.file, matchedFiles: files.length }),
237
+ ...(options.in && { scopedTo: options.in }),
238
+ ...(emptyFiles > 0 && fileFilter && { emptyFiles })
239
+ },
240
+ totals: {
241
+ files: files.length,
242
+ lines: totalLines,
243
+ functions: totalFunctions,
244
+ classes: totalClasses,
245
+ state: totalState,
246
+ testFiles: totalTests
247
+ },
248
+ summary: {
249
+ topFunctionFiles,
250
+ topLineFiles,
251
+ entryFiles
252
+ },
253
+ files: displayFiles,
254
+ hiddenFiles
255
+ };
256
+ }
257
+
258
+ module.exports = { getStats, getToc };