ucn 4.0.2 → 4.1.1

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 (46) hide show
  1. package/.claude/skills/ucn/SKILL.md +31 -7
  2. package/README.md +90 -51
  3. package/cli/index.js +199 -94
  4. package/core/account.js +3 -1
  5. package/core/analysis.js +322 -304
  6. package/core/bridge.js +13 -8
  7. package/core/cache.js +109 -19
  8. package/core/callers.js +969 -77
  9. package/core/check.js +19 -8
  10. package/core/deadcode.js +368 -40
  11. package/core/discovery.js +31 -11
  12. package/core/entrypoints.js +149 -17
  13. package/core/execute.js +330 -43
  14. package/core/graph-build.js +61 -10
  15. package/core/graph.js +282 -61
  16. package/core/imports.js +72 -3
  17. package/core/output/analysis-ext.js +70 -10
  18. package/core/output/analysis.js +52 -33
  19. package/core/output/check.js +4 -1
  20. package/core/output/endpoints.js +8 -3
  21. package/core/output/extraction.js +12 -1
  22. package/core/output/find.js +23 -9
  23. package/core/output/graph.js +32 -9
  24. package/core/output/refactoring.js +147 -49
  25. package/core/output/reporting.js +104 -5
  26. package/core/output/search.js +30 -3
  27. package/core/output/shared.js +31 -4
  28. package/core/output/tracing.js +22 -11
  29. package/core/parser.js +8 -6
  30. package/core/project.js +167 -7
  31. package/core/registry.js +20 -16
  32. package/core/reporting.js +131 -13
  33. package/core/search.js +270 -55
  34. package/core/shared.js +240 -1
  35. package/core/stacktrace.js +23 -1
  36. package/core/tracing.js +278 -36
  37. package/core/verify.js +352 -349
  38. package/languages/go.js +29 -17
  39. package/languages/index.js +56 -0
  40. package/languages/java.js +133 -16
  41. package/languages/javascript.js +215 -27
  42. package/languages/python.js +113 -47
  43. package/languages/rust.js +89 -26
  44. package/languages/utils.js +35 -7
  45. package/mcp/server.js +69 -30
  46. package/package.json +5 -1
package/core/reporting.js CHANGED
@@ -8,6 +8,8 @@
8
8
  'use strict';
9
9
 
10
10
  const fs = require('fs');
11
+ const { codeUnitCompare, CALLABLE_SYMBOL_KINDS } = require('./shared');
12
+ const { _declaredFieldType, _projectTopLevelNames } = require('./callers');
11
13
  const path = require('path');
12
14
  const { isTestFile } = require('./discovery');
13
15
 
@@ -67,9 +69,7 @@ function getStats(index, options = {}) {
67
69
  const functions = [];
68
70
  for (const [name, symbols] of index.symbols) {
69
71
  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') {
72
+ if (CALLABLE_SYMBOL_KINDS.has(sym.type)) {
73
73
  const lineCount = sym.endLine - sym.startLine + 1;
74
74
  const relativePath = sym.relativePath || (sym.file ? path.relative(index.root, sym.file) : '');
75
75
  functions.push({
@@ -97,10 +97,7 @@ function getStats(index, options = {}) {
97
97
  const top = options.top === 0
98
98
  ? 0
99
99
  : ((options.top != null && Number(options.top) > 0) ? Number(options.top) : 10);
100
- const FUNCTION_TYPES = new Set([
101
- 'function', 'method', 'static', 'constructor',
102
- 'public', 'abstract', 'classmethod'
103
- ]);
100
+ const FUNCTION_TYPES = CALLABLE_SYMBOL_KINDS;
104
101
 
105
102
  // Ensure the calls cache is fully populated before counting.
106
103
  // First-time stats --hot may need to parse files to extract calls;
@@ -135,6 +132,15 @@ function getStats(index, options = {}) {
135
132
  // Pre-compute import-alias sets per file. Used to distinguish `mod.foo()`
136
133
  // (resolves to top-level foo) from `obj.foo()` on a local variable.
137
134
  const fileImportAliases = new Map(); // filePath -> Set<string> of alias names
135
+ const fieldHopCache = new Map(); // rootType\0field -> declared type|null
136
+ // Names import-bound to an EXTERNAL module, per file (fix #256,
137
+ // dogfood-measured: 895 node:test `describe(...)` calls in test
138
+ // files were attributed to a project closure named `describe` —
139
+ // the #215 name discipline says an externally-bound bare name
140
+ // cannot reach a project def, so it never counts toward the hot
141
+ // leaderboard). Relative modules, resolved modules, and resolver
142
+ // gaps (first segment names a project path) all stay countable.
143
+ const fileExternalNames = new Map(); // filePath -> Set<string>
138
144
  for (const [filePath, fileEntry] of index.files) {
139
145
  const aliases = new Set();
140
146
  // importNames are the named imports/exports brought into this file.
@@ -147,6 +153,16 @@ function getStats(index, options = {}) {
147
153
  }
148
154
  }
149
155
  fileImportAliases.set(filePath, aliases);
156
+ let ext = null;
157
+ for (const b of (fileEntry.importBindings || [])) {
158
+ const mod = String(b.module || '');
159
+ if (!b.name || !mod || mod.startsWith('.') || mod.startsWith('/')) continue;
160
+ if (fileEntry.moduleResolved && fileEntry.moduleResolved[mod]) continue;
161
+ const firstSeg = mod.split(/[./]/).filter(Boolean)[0];
162
+ if (firstSeg && _projectTopLevelNames(index).has(firstSeg)) continue;
163
+ (ext || (ext = new Set())).add(b.name);
164
+ }
165
+ if (ext) fileExternalNames.set(filePath, ext);
150
166
  }
151
167
 
152
168
  for (const [filePath, entry] of index.callsCache) {
@@ -164,6 +180,9 @@ function getStats(index, options = {}) {
164
180
  // Bare-name call: foo() or pkg.Foo() (Go package call has receiver
165
181
  // but isMethod:false — keep counting under bareName since they
166
182
  // resolve like top-level functions in their package).
183
+ // Externally-bound names are the external library's calls,
184
+ // never a project def's (fix #256).
185
+ if (fileExternalNames.get(filePath)?.has(c.name)) continue;
167
186
  bareNameCounts.set(c.name, (bareNameCounts.get(c.name) || 0) + 1);
168
187
  } else if (isSelfMethod) {
169
188
  // self/this.foo() — attributed to the enclosing class's foo
@@ -178,11 +197,26 @@ function getStats(index, options = {}) {
178
197
  importedReceiverCounts.set(c.name,
179
198
  (importedReceiverCounts.get(c.name) || 0) + 1);
180
199
  }
181
- if (c.receiverType) {
182
- let inner = methodByReceiverType.get(c.receiverType);
200
+ // Field-access receivers (fix #251): `tm.service.Save()`
201
+ // carries receiverRootType, not receiverType — the same
202
+ // #202/#231 declared-field hop the caller/callee engine
203
+ // uses. Without it, edges `context` confirms were
204
+ // invisible to the hot leaderboard.
205
+ let recvType = c.receiverType;
206
+ if (!recvType && c.receiverField && c.receiverRootType) {
207
+ const hopKey = `${c.receiverRootType}\u0000${c.receiverField}`;
208
+ if (!fieldHopCache.has(hopKey)) {
209
+ const lang = index.files.get(filePath)?.language;
210
+ fieldHopCache.set(hopKey,
211
+ lang ? _declaredFieldType(index, c.receiverRootType, c.receiverField, lang) : null);
212
+ }
213
+ recvType = fieldHopCache.get(hopKey);
214
+ }
215
+ if (recvType) {
216
+ let inner = methodByReceiverType.get(recvType);
183
217
  if (!inner) {
184
218
  inner = new Map();
185
- methodByReceiverType.set(c.receiverType, inner);
219
+ methodByReceiverType.set(recvType, inner);
186
220
  }
187
221
  inner.set(c.name, (inner.get(c.name) || 0) + 1);
188
222
  }
@@ -304,7 +338,7 @@ function getStats(index, options = {}) {
304
338
  if (approximate) usedHeuristicSplit = true;
305
339
  // Sort locations by (file, startLine) for stable display.
306
340
  locations.sort((a, b) =>
307
- a.file.localeCompare(b.file) ||
341
+ codeUnitCompare(a.file, b.file) ||
308
342
  (a.startLine || 0) - (b.startLine || 0)
309
343
  );
310
344
  const primary = locations[0];
@@ -329,7 +363,7 @@ function getStats(index, options = {}) {
329
363
  // Stable order: callCount desc, then (relativePath, startLine) asc.
330
364
  hotList.sort((a, b) =>
331
365
  (b.callCount - a.callCount) ||
332
- a.file.localeCompare(b.file) ||
366
+ codeUnitCompare(a.file, b.file) ||
333
367
  (a.startLine || 0) - (b.startLine || 0)
334
368
  );
335
369
 
@@ -736,4 +770,88 @@ function computeCoverageSample(index, { sampleSize, inFilter, matchInFilter }) {
736
770
  return buckets;
737
771
  }
738
772
 
739
- module.exports = { getStats, getToc, doctor };
773
+ /**
774
+ * orient — one-call cold-repo orientation: size + language mix, densest
775
+ * directories, most-called functions, entry-point counts, and the doctor
776
+ * trust verdict. Composes existing engine reads; counts and pointers only
777
+ * (no caller claims, so no account — the toc/stats category).
778
+ */
779
+ function orient(index, options = {}) {
780
+ const top = options.top || 8;
781
+ // Fetch a deeper hot list so production functions survive the filter
782
+ // below even when test helpers dominate raw call counts.
783
+ const stats = getStats(index, { hot: true, top: Math.min(top * 5, 200) });
784
+ const health = doctor(index, {});
785
+
786
+ // Densest directories (leaf dirname rollup — "where does the code live")
787
+ const dirMap = new Map();
788
+ for (const [, fe] of index.files) {
789
+ const rp = fe.relativePath;
790
+ if (!rp) continue;
791
+ const slash = rp.lastIndexOf('/');
792
+ const dir = slash === -1 ? '.' : rp.slice(0, slash);
793
+ const e = dirMap.get(dir) || { dir, files: 0, symbols: 0 };
794
+ e.files += 1;
795
+ e.symbols += (fe.symbols || []).length;
796
+ dirMap.set(dir, e);
797
+ }
798
+ const dirs = [...dirMap.values()]
799
+ .sort((a, b) => b.symbols - a.symbols || codeUnitCompare(a.dir, b.dir))
800
+ .slice(0, top);
801
+
802
+ // Entry-point counts by type — orientation must not fail on detection
803
+ let entrypoints = null;
804
+ try {
805
+ const { detectEntrypoints } = require('./entrypoints');
806
+ const eps = detectEntrypoints(index, {});
807
+ if (Array.isArray(eps)) {
808
+ const byType = new Map();
809
+ for (const e of eps) byType.set(e.type, (byType.get(e.type) || 0) + 1);
810
+ entrypoints = {
811
+ total: eps.length,
812
+ byType: [...byType.entries()]
813
+ .map(([type, count]) => ({ type, count }))
814
+ .sort((a, b) => b.count - a.count || codeUnitCompare(a.type, b.type)),
815
+ };
816
+ }
817
+ } catch { /* detection error → entrypoints stays null, rendered as unavailable */ }
818
+
819
+ // Orientation wants the ENGINE's hot functions, not fixture helpers —
820
+ // prefer production-path entries (labeled as such by the formatter);
821
+ // an all-test project falls back to the raw ranking.
822
+ const { isTestPath } = require('./shared');
823
+ const allHot = (stats.hot?.items || []).map(i => ({
824
+ name: i.name,
825
+ file: i.file,
826
+ line: i.startLine,
827
+ callCount: i.callCount,
828
+ ...(i.className ? { className: i.className } : {}),
829
+ }));
830
+ const prodHot = allHot.filter(i => i.file && !isTestPath(i.file));
831
+ const production = prodHot.length > 0;
832
+ const hotItems = (production ? prodHot : allHot).slice(0, top);
833
+ const hottestProd = hotItems[0] || null;
834
+
835
+ return {
836
+ root: stats.root,
837
+ files: stats.files,
838
+ symbols: stats.symbols,
839
+ buildTime: stats.buildTime,
840
+ byLanguage: stats.byLanguage,
841
+ dirs,
842
+ hot: { total: stats.hot?.total ?? 0, top, production, items: hotItems },
843
+ entrypoints,
844
+ trust: {
845
+ level: health.trust,
846
+ blindSpots: {
847
+ dynamicImports: health.blindSpots?.dynamicImports?.count ?? 0,
848
+ evalCalls: health.blindSpots?.evalCalls?.count ?? 0,
849
+ reflection: health.blindSpots?.reflection?.count ?? 0,
850
+ parseFailures: health.blindSpots?.parseFailures?.count ?? 0,
851
+ },
852
+ },
853
+ suggest: hottestProd ? hottestProd.name : null,
854
+ };
855
+ }
856
+
857
+ module.exports = { getStats, getToc, doctor, orient };