ucn 3.8.13 → 3.8.15

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 +13 -1
  3. package/README.md +1 -0
  4. package/cli/index.js +165 -246
  5. package/core/analysis.js +1400 -0
  6. package/core/build-worker.js +194 -0
  7. package/core/cache.js +105 -7
  8. package/core/callers.js +194 -64
  9. package/core/deadcode.js +22 -66
  10. package/core/discovery.js +9 -54
  11. package/core/execute.js +139 -54
  12. package/core/graph.js +615 -0
  13. package/core/imports.js +50 -16
  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/verify.js CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  const path = require('path');
9
- const { detectLanguage, getParser, getLanguageModule, safeParse } = require('../languages');
9
+ const { detectLanguage, getParser, getLanguageModule, safeParse, langTraits } = require('../languages');
10
10
  const { escapeRegExp } = require('./shared');
11
11
  const { extractImports } = require('./imports');
12
12
 
@@ -164,8 +164,9 @@ function identifyCallPatterns(callSites, funcName) {
164
164
  if (new RegExp('\\.' + escapeRegExp(funcName) + '\\s*\\(').test(expr)) patterns.chainedCalls++;
165
165
 
166
166
  if (site.args && site.args.length > 0) {
167
+ const literalPattern = /^[\d'"{\[]/; // eslint-disable-line no-useless-escape
167
168
  const hasLiteral = site.args.some(a =>
168
- /^[\d'"{\[]/.test(a) || a === 'true' || a === 'false' || a === 'null'
169
+ literalPattern.test(a) || a === 'true' || a === 'false' || a === 'null'
169
170
  );
170
171
  if (hasLiteral) patterns.constantArgs++;
171
172
  if (site.hasVariable) patterns.variableArgs++;
@@ -194,11 +195,9 @@ function verify(index, name, options = {}) {
194
195
  const fileEntry = index.files.get(def.file);
195
196
  const lang = fileEntry?.language;
196
197
  let params = def.paramsStructured || [];
197
- if ((lang === 'python' || lang === 'rust') && params.length > 0) {
198
- const firstName = params[0].name;
199
- if (firstName === 'self' || firstName === 'cls' || firstName === '&self' || firstName === '&mut self' || firstName === 'mut self') {
200
- params = params.slice(1);
201
- }
198
+ const selfParams = langTraits(lang)?.selfParam;
199
+ if (selfParams && params.length > 0 && selfParams.includes(params[0].name)) {
200
+ params = params.slice(1);
202
201
  }
203
202
  const hasRest = params.some(p => p.rest);
204
203
  // Rest params don't count toward expected/min — they accept 0+ extra args
@@ -345,7 +344,7 @@ function verify(index, name, options = {}) {
345
344
  const importedNames = getImportedNames(call.file);
346
345
  if (!importedNames.has(callReceiver)) continue;
347
346
  // Receiver matches target module and is imported — keep it
348
- } else if (callReceiver && defLang === 'go') {
347
+ } else if (callReceiver && langTraits(defLang)?.hasReceiverPackageCalls) {
349
348
  // Go: receiver is package alias (last segment of import path, e.g., "controller"
350
349
  // from "k8s.io/.../pkg/controller"), not the filename ("controller_utils").
351
350
  // Check if receiver matches the directory name of the target file.
@@ -588,11 +587,9 @@ function plan(index, name, options = {}) {
588
587
  const fileEntry = index.files.get(def.file);
589
588
  const lang = fileEntry?.language;
590
589
  let selfOffset = 0;
591
- if ((lang === 'python' || lang === 'rust') && currentParams.length > 0) {
592
- const firstName = currentParams[0].name;
593
- if (firstName === 'self' || firstName === 'cls' || firstName === '&self' || firstName === '&mut self' || firstName === 'mut self') {
594
- selfOffset = 1;
595
- }
590
+ const planSelfParams = langTraits(lang)?.selfParam;
591
+ if (planSelfParams && currentParams.length > 0 && planSelfParams.includes(currentParams[0].name)) {
592
+ selfOffset = 1;
596
593
  }
597
594
  const callerArgIndex = paramIndex - selfOffset;
598
595
 
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+
3
+ const js = require('@eslint/js');
4
+
5
+ module.exports = [
6
+ js.configs.recommended,
7
+ {
8
+ languageOptions: {
9
+ ecmaVersion: 2020,
10
+ sourceType: 'commonjs',
11
+ globals: {
12
+ // Node.js globals
13
+ require: 'readonly',
14
+ module: 'readonly',
15
+ exports: 'readonly',
16
+ __dirname: 'readonly',
17
+ __filename: 'readonly',
18
+ process: 'readonly',
19
+ console: 'readonly',
20
+ Buffer: 'readonly',
21
+ setTimeout: 'readonly',
22
+ setInterval: 'readonly',
23
+ clearTimeout: 'readonly',
24
+ clearInterval: 'readonly',
25
+ setImmediate: 'readonly',
26
+ URL: 'readonly',
27
+ URLSearchParams: 'readonly',
28
+ TextEncoder: 'readonly',
29
+ TextDecoder: 'readonly',
30
+ },
31
+ },
32
+ rules: {
33
+ 'no-undef': 'error',
34
+ 'no-unused-vars': ['warn', { args: 'none', caughtErrors: 'none' }],
35
+ 'no-redeclare': 'error',
36
+ 'eqeqeq': ['warn', 'smart'],
37
+ 'no-constant-condition': ['error', { checkLoops: false }],
38
+ },
39
+ },
40
+ {
41
+ ignores: ['node_modules/', 'test/', '.ucn-cache/', 'demo/'],
42
+ },
43
+ ];
package/jsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "compilerOptions": {
3
+ "checkJs": false,
4
+ "module": "commonjs",
5
+ "target": "es2020",
6
+ "baseUrl": "."
7
+ },
8
+ "include": ["core/**/*.js", "cli/**/*.js", "mcp/**/*.js", "languages/**/*.js"],
9
+ "exclude": ["node_modules"]
10
+ }
package/languages/go.js CHANGED
@@ -1124,8 +1124,15 @@ function findImportsInCode(code, parser) {
1124
1124
  }
1125
1125
 
1126
1126
  if (modulePath) {
1127
- // Package name is last segment of path
1128
- const pkgName = alias || modulePath.split('/').pop();
1127
+ // Package name is last segment of path, skipping Go version suffixes (v2, v3, etc.)
1128
+ let pkgName = alias;
1129
+ if (!pkgName) {
1130
+ const parts = modulePath.split('/');
1131
+ // Go convention: if last segment matches /^v\d+$/, use the previous segment
1132
+ // e.g., k8s.io/klog/v2 → klog, github.com/foo/bar/v3 → bar
1133
+ const last = parts[parts.length - 1];
1134
+ pkgName = (/^v\d+$/.test(last) && parts.length > 1) ? parts[parts.length - 2] : last;
1135
+ }
1129
1136
  imports.push({
1130
1137
  module: modulePath,
1131
1138
  names: [pkgName],
@@ -1345,6 +1352,17 @@ function findUsagesInCode(code, name, parser) {
1345
1352
  return usages;
1346
1353
  }
1347
1354
 
1355
+ /**
1356
+ * Check if a symbol is a Go-convention entry point.
1357
+ * These are invoked by the Go runtime or test runner, not user code.
1358
+ */
1359
+ function isEntryPoint(symbol) {
1360
+ const { name } = symbol;
1361
+ if (name === 'main' || name === 'init') return true;
1362
+ if (/^(Test|Benchmark|Example|Fuzz)[A-Z_]/.test(name)) return true;
1363
+ return false;
1364
+ }
1365
+
1348
1366
  module.exports = {
1349
1367
  findFunctions,
1350
1368
  findClasses,
@@ -1353,5 +1371,6 @@ module.exports = {
1353
1371
  findImportsInCode,
1354
1372
  findExportsInCode,
1355
1373
  findUsagesInCode,
1374
+ isEntryPoint,
1356
1375
  parse
1357
1376
  };
package/languages/html.js CHANGED
@@ -311,6 +311,13 @@ function findUsagesInCode(code, name, parser) {
311
311
  return jsUsages.concat(handlerUsages);
312
312
  }
313
313
 
314
+ /**
315
+ * HTML has no entry points of its own.
316
+ */
317
+ function isEntryPoint() {
318
+ return false;
319
+ }
320
+
314
321
  module.exports = {
315
322
  parse,
316
323
  findFunctions,
@@ -322,6 +329,7 @@ module.exports = {
322
329
  findImportsInCode,
323
330
  findExportsInCode,
324
331
  findUsagesInCode,
332
+ isEntryPoint,
325
333
  // Exported for testing
326
334
  extractScriptBlocks,
327
335
  buildVirtualJSContent,
@@ -12,55 +12,133 @@ let TreeSitter = null;
12
12
  // Cached parser instances
13
13
  const parsers = {};
14
14
 
15
+ // Shared trait presets for languages with the same type-system characteristics
16
+ const STRUCTURAL_TRAITS = {
17
+ typeSystem: 'structural',
18
+ methodCallInclusion: 'explicit',
19
+ packageScope: 'file',
20
+ hasReceiverPackageCalls: false,
21
+ exportVisibility: 'keyword',
22
+ hasDynamicImports: true,
23
+ testDirs: [],
24
+ };
25
+ const NOMINAL_TRAITS = {
26
+ typeSystem: 'nominal',
27
+ methodCallInclusion: 'auto',
28
+ packageScope: 'file',
29
+ hasReceiverPackageCalls: false,
30
+ exportVisibility: 'keyword',
31
+ hasDynamicImports: true,
32
+ testDirs: [],
33
+ };
34
+
15
35
  // Language configurations
16
36
  const LANGUAGES = {
17
37
  javascript: {
18
38
  name: 'javascript',
19
39
  extensions: ['.js', '.jsx', '.mjs', '.cjs'],
20
40
  treeSitterLang: 'javascript',
21
- module: () => require('./javascript')
41
+ module: () => require('./javascript'),
42
+ treeSitterModule: () => require('tree-sitter-javascript'),
43
+ traits: {
44
+ ...STRUCTURAL_TRAITS,
45
+ selfParam: ['this'],
46
+ testFileCandidates: (base, ext) => [`${base}.test${ext}`, `${base}.spec${ext}`, `${base}.test.ts`, `${base}.test.js`, `${base}.spec.ts`, `${base}.spec.js`],
47
+ testDirs: ['__tests__'],
48
+ },
22
49
  },
23
50
  typescript: {
24
51
  name: 'typescript',
25
52
  extensions: ['.ts'],
26
53
  treeSitterLang: 'typescript',
27
- module: () => require('./javascript') // Same module, different parser
54
+ module: () => require('./javascript'), // Same module, different parser
55
+ treeSitterModule: () => require('tree-sitter-typescript').typescript,
56
+ traits: {
57
+ ...STRUCTURAL_TRAITS,
58
+ selfParam: ['this'],
59
+ testFileCandidates: (base, ext) => [`${base}.test${ext}`, `${base}.spec${ext}`, `${base}.test.ts`, `${base}.test.js`, `${base}.spec.ts`, `${base}.spec.js`],
60
+ testDirs: ['__tests__'],
61
+ },
28
62
  },
29
63
  tsx: {
30
64
  name: 'tsx',
31
65
  extensions: ['.tsx'],
32
66
  treeSitterLang: 'tsx',
33
- module: () => require('./javascript')
67
+ module: () => require('./javascript'),
68
+ treeSitterModule: () => require('tree-sitter-typescript').tsx,
69
+ traits: {
70
+ ...STRUCTURAL_TRAITS,
71
+ selfParam: ['this'],
72
+ testFileCandidates: (base, ext) => [`${base}.test${ext}`, `${base}.spec${ext}`, `${base}.test.ts`, `${base}.test.js`, `${base}.spec.ts`, `${base}.spec.js`],
73
+ testDirs: ['__tests__'],
74
+ },
34
75
  },
35
76
  python: {
36
77
  name: 'python',
37
78
  extensions: ['.py', '.pyi'],
38
79
  treeSitterLang: 'python',
39
- module: () => require('./python')
80
+ module: () => require('./python'),
81
+ treeSitterModule: () => require('tree-sitter-python'),
82
+ traits: {
83
+ ...STRUCTURAL_TRAITS,
84
+ selfParam: ['self', 'cls'],
85
+ testFileCandidates: (base, ext) => [`test_${base}.py`, `${base}_test.py`],
86
+ testDirs: ['tests'],
87
+ },
40
88
  },
41
89
  go: {
42
90
  name: 'go',
43
91
  extensions: ['.go'],
44
92
  treeSitterLang: 'go',
45
- module: () => require('./go')
93
+ module: () => require('./go'),
94
+ treeSitterModule: () => require('tree-sitter-go'),
95
+ traits: {
96
+ ...NOMINAL_TRAITS,
97
+ selfParam: null,
98
+ packageScope: 'directory',
99
+ hasReceiverPackageCalls: true,
100
+ exportVisibility: 'capitalization',
101
+ hasDynamicImports: false,
102
+ testFileCandidates: (base, ext) => [`${base}_test.go`],
103
+ },
46
104
  },
47
105
  rust: {
48
106
  name: 'rust',
49
107
  extensions: ['.rs'],
50
108
  treeSitterLang: 'rust',
51
- module: () => require('./rust')
109
+ module: () => require('./rust'),
110
+ treeSitterModule: () => require('tree-sitter-rust'),
111
+ traits: {
112
+ ...NOMINAL_TRAITS,
113
+ selfParam: ['self', '&self', '&mut self', 'mut self'],
114
+ hasDynamicImports: false,
115
+ testFileCandidates: (base, ext) => [`${base}_test.rs`],
116
+ testDirs: ['tests'],
117
+ },
52
118
  },
53
119
  java: {
54
120
  name: 'java',
55
121
  extensions: ['.java'],
56
122
  treeSitterLang: 'java',
57
- module: () => require('./java')
123
+ module: () => require('./java'),
124
+ treeSitterModule: () => require('tree-sitter-java'),
125
+ traits: {
126
+ ...NOMINAL_TRAITS,
127
+ selfParam: ['this'],
128
+ testFileCandidates: (base, ext) => [`${base}Test.java`, `${base}Tests.java`, `${base}TestCase.java`],
129
+ },
58
130
  },
59
131
  html: {
60
132
  name: 'html',
61
133
  extensions: ['.html', '.htm'],
62
134
  treeSitterLang: 'html',
63
- module: () => require('./html')
135
+ module: () => require('./html'),
136
+ treeSitterModule: () => require('tree-sitter-html'),
137
+ traits: {
138
+ ...STRUCTURAL_TRAITS,
139
+ selfParam: ['this'],
140
+ testFileCandidates: (base, ext) => [`${base}.test${ext}`, `${base}.spec${ext}`],
141
+ },
64
142
  }
65
143
  };
66
144
 
@@ -83,7 +161,8 @@ function loadTreeSitter() {
83
161
  } catch (e) {
84
162
  throw new Error(
85
163
  'tree-sitter is required but not installed.\n' +
86
- 'Install with: npm install'
164
+ 'Install with: npm install',
165
+ { cause: e }
87
166
  );
88
167
  }
89
168
  }
@@ -107,41 +186,14 @@ function getParser(language) {
107
186
  }
108
187
 
109
188
  try {
110
- let lang;
111
- switch (language) {
112
- case 'javascript':
113
- lang = require('tree-sitter-javascript');
114
- break;
115
- case 'typescript':
116
- lang = require('tree-sitter-typescript').typescript;
117
- break;
118
- case 'tsx':
119
- lang = require('tree-sitter-typescript').tsx;
120
- break;
121
- case 'python':
122
- lang = require('tree-sitter-python');
123
- break;
124
- case 'go':
125
- lang = require('tree-sitter-go');
126
- break;
127
- case 'java':
128
- lang = require('tree-sitter-java');
129
- break;
130
- case 'rust':
131
- lang = require('tree-sitter-rust');
132
- break;
133
- case 'html':
134
- lang = require('tree-sitter-html');
135
- break;
136
- default:
137
- throw new Error(`No tree-sitter grammar for: ${language}`);
138
- }
189
+ const lang = config.treeSitterModule();
139
190
  parser.setLanguage(lang);
140
191
  } catch (e) {
141
192
  throw new Error(
142
- `Failed to load tree-sitter-${language}.\n` +
193
+ `Failed to load tree-sitter grammar for ${language}.\n` +
143
194
  `Install with: npm install tree-sitter-${language}\n` +
144
- `Original error: ${e.message}`
195
+ `Original error: ${e.message}`,
196
+ { cause: e }
145
197
  );
146
198
  }
147
199
 
@@ -282,6 +334,15 @@ function safeParse(parser, content, oldTree = undefined, options = {}) {
282
334
  throw lastError;
283
335
  }
284
336
 
337
+ /**
338
+ * Get trait object for a language.
339
+ * @param {string} language - Language name (e.g. 'go', 'python')
340
+ * @returns {object|undefined} Trait object or undefined if unknown language
341
+ */
342
+ function langTraits(language) {
343
+ return LANGUAGES[language]?.traits;
344
+ }
345
+
285
346
  module.exports = {
286
347
  detectLanguage,
287
348
  getParser,
@@ -293,6 +354,7 @@ module.exports = {
293
354
  PARSE_OPTIONS,
294
355
  getParseOptions,
295
356
  safeParse,
357
+ langTraits,
296
358
  DEFAULT_BUFFER_SIZE,
297
359
  MAX_BUFFER_SIZE
298
360
  };
package/languages/java.js CHANGED
@@ -1173,6 +1173,18 @@ function findUsagesInCode(code, name, parser) {
1173
1173
  return usages;
1174
1174
  }
1175
1175
 
1176
+ /**
1177
+ * Check if a symbol is a Java-convention entry point.
1178
+ * These are invoked by the JVM runtime, test runners, or required by type system.
1179
+ */
1180
+ function isEntryPoint(symbol) {
1181
+ const m = symbol.modifiers || [];
1182
+ if (symbol.name === 'main' && m.includes('public') && m.includes('static')) return true;
1183
+ if (m.includes('test')) return true;
1184
+ if (m.includes('override')) return true;
1185
+ return false;
1186
+ }
1187
+
1176
1188
  module.exports = {
1177
1189
  findFunctions,
1178
1190
  findClasses,
@@ -1181,5 +1193,6 @@ module.exports = {
1181
1193
  findImportsInCode,
1182
1194
  findExportsInCode,
1183
1195
  findUsagesInCode,
1196
+ isEntryPoint,
1184
1197
  parse
1185
1198
  };
@@ -1776,7 +1776,7 @@ function findImportsInCode(code, parser) {
1776
1776
  const firstArg = argsNode.namedChild(0);
1777
1777
  const line = node.startPosition.row + 1;
1778
1778
  const names = [];
1779
- let modulePath = null;
1779
+ let modulePath;
1780
1780
  let dynamic = false;
1781
1781
 
1782
1782
  if (firstArg && firstArg.type === 'string') {
@@ -2141,6 +2141,21 @@ function findUsagesInCode(code, name, parser) {
2141
2141
  return usages;
2142
2142
  }
2143
2143
 
2144
+ const _JS_LIFECYCLE_METHODS = new Set([
2145
+ 'render', 'componentDidMount', 'componentDidUpdate', 'componentWillUnmount',
2146
+ 'getDerivedStateFromProps', 'getDerivedStateFromError', 'componentDidCatch',
2147
+ 'getSnapshotBeforeUpdate', 'shouldComponentUpdate',
2148
+ 'connectedCallback', 'disconnectedCallback', 'attributeChangedCallback', 'adoptedCallback'
2149
+ ]);
2150
+
2151
+ /**
2152
+ * Check if a symbol is a JS/TS-convention entry point.
2153
+ * These are framework lifecycle methods invoked by React or Web Components.
2154
+ */
2155
+ function isEntryPoint(symbol) {
2156
+ return !!(symbol.isMethod && _JS_LIFECYCLE_METHODS.has(symbol.name));
2157
+ }
2158
+
2144
2159
  module.exports = {
2145
2160
  findFunctions,
2146
2161
  findClasses,
@@ -2151,5 +2166,6 @@ module.exports = {
2151
2166
  findImportsInCode,
2152
2167
  findExportsInCode,
2153
2168
  findUsagesInCode,
2169
+ isEntryPoint,
2154
2170
  parse
2155
2171
  };
@@ -1198,6 +1198,19 @@ function extractConstructorName(node) {
1198
1198
  return null;
1199
1199
  }
1200
1200
 
1201
+ /**
1202
+ * Check if a symbol is a Python-convention entry point.
1203
+ * These are invoked by the Python runtime, test runners, or frameworks.
1204
+ */
1205
+ function isEntryPoint(symbol) {
1206
+ const { name } = symbol;
1207
+ if (/^__\w+__$/.test(name)) return true;
1208
+ if (/^test_/.test(name)) return true;
1209
+ if (/^(setUp|tearDown)(Class|Module)?$/.test(name)) return true;
1210
+ if (/^pytest_/.test(name)) return true;
1211
+ return false;
1212
+ }
1213
+
1201
1214
  module.exports = {
1202
1215
  findFunctions,
1203
1216
  findClasses,
@@ -1207,5 +1220,6 @@ module.exports = {
1207
1220
  findExportsInCode,
1208
1221
  findUsagesInCode,
1209
1222
  findInstanceAttributeTypes,
1223
+ isEntryPoint,
1210
1224
  parse
1211
1225
  };
package/languages/rust.js CHANGED
@@ -1459,6 +1459,18 @@ function findUsagesInCode(code, name, parser) {
1459
1459
  return usages;
1460
1460
  }
1461
1461
 
1462
+ /**
1463
+ * Check if a symbol is a Rust-convention entry point.
1464
+ * These are invoked by the Rust runtime, test harness, or required by trait contracts.
1465
+ */
1466
+ function isEntryPoint(symbol) {
1467
+ const m = symbol.modifiers || [];
1468
+ if (symbol.name === 'main') return true;
1469
+ if (m.includes('test') || m.includes('bench')) return true;
1470
+ if (symbol.isMethod && symbol.className && symbol.traitImpl) return true;
1471
+ return false;
1472
+ }
1473
+
1462
1474
  module.exports = {
1463
1475
  findFunctions,
1464
1476
  findClasses,
@@ -1467,5 +1479,6 @@ module.exports = {
1467
1479
  findImportsInCode,
1468
1480
  findExportsInCode,
1469
1481
  findUsagesInCode,
1482
+ isEntryPoint,
1470
1483
  parse
1471
1484
  };
@@ -372,7 +372,7 @@ function extractRustDocstring(codeOrLines, startLine) {
372
372
  commentStart--;
373
373
  }
374
374
  // Return first line of comment block
375
- const firstLine = lines[commentStart].trim().replace(/^\/\/[\/!]\s?/, '');
375
+ const firstLine = lines[commentStart].trim().replace(/^\/\/[/!]\s?/, '');
376
376
  if (firstLine) return firstLine;
377
377
  }
378
378
  return null;