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.
- package/.claude/skills/ucn/SKILL.md +3 -1
- package/.github/workflows/ci.yml +15 -3
- package/.github/workflows/publish.yml +20 -8
- package/README.md +1 -0
- package/cli/index.js +165 -246
- package/core/analysis.js +1400 -0
- package/core/build-worker.js +194 -0
- package/core/cache.js +105 -7
- package/core/callers.js +194 -64
- package/core/deadcode.js +22 -66
- package/core/discovery.js +9 -54
- package/core/execute.js +139 -54
- package/core/graph.js +615 -0
- package/core/output/analysis-ext.js +271 -0
- package/core/output/analysis.js +491 -0
- package/core/output/extraction.js +188 -0
- package/core/output/find.js +355 -0
- package/core/output/graph.js +399 -0
- package/core/output/refactoring.js +293 -0
- package/core/output/reporting.js +331 -0
- package/core/output/search.js +307 -0
- package/core/output/shared.js +271 -0
- package/core/output/tracing.js +416 -0
- package/core/output.js +15 -3293
- package/core/parallel-build.js +165 -0
- package/core/project.js +299 -3633
- package/core/registry.js +59 -0
- package/core/reporting.js +258 -0
- package/core/search.js +890 -0
- package/core/stacktrace.js +1 -1
- package/core/tracing.js +631 -0
- package/core/verify.js +10 -13
- package/eslint.config.js +43 -0
- package/jsconfig.json +10 -0
- package/languages/go.js +21 -2
- package/languages/html.js +8 -0
- package/languages/index.js +102 -40
- package/languages/java.js +13 -0
- package/languages/javascript.js +17 -1
- package/languages/python.js +14 -0
- package/languages/rust.js +13 -0
- package/languages/utils.js +1 -1
- package/mcp/server.js +45 -28
- 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 };
|