ucn 4.0.0 → 4.0.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.
package/cli/index.js CHANGED
@@ -1052,7 +1052,8 @@ function runProjectCommand(rootDir, command, arg) {
1052
1052
  r => output.formatDeadcode(r, {
1053
1053
  top: flags.top,
1054
1054
  decoratedHint: !flags.includeDecorated && result.excludedDecorated > 0 ? `${result.excludedDecorated} decorated/annotated symbol(s) hidden (framework-registered). Use --include-decorated to include them.` : undefined,
1055
- exportedHint: !flags.includeExported && result.excludedExported > 0 ? `${result.excludedExported} exported symbol(s) excluded (all have callers). Use --include-exported to audit them.` : undefined
1055
+ exportedHint: !flags.includeExported && result.excludedExported > 0 ? `${result.excludedExported} exported symbol(s) excluded (all have callers). Use --include-exported to audit them.` : undefined,
1056
+ externalContractHint: !flags.includeExported && result.excludedExternalContract > 0 ? `${result.excludedExternalContract} symbol(s) hidden (override an out-of-tree base class — reachable via external contract, not dead). Use --include-exported to include them.` : undefined
1056
1057
  })
1057
1058
  );
1058
1059
  break;
@@ -1911,7 +1912,8 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
1911
1912
  console.log(output.formatDeadcode(result, {
1912
1913
  top: iflags.top,
1913
1914
  decoratedHint: !iflags.includeDecorated && result.excludedDecorated > 0 ? `${result.excludedDecorated} decorated/annotated symbol(s) hidden (framework-registered). Use --include-decorated to include them.` : undefined,
1914
- exportedHint: !iflags.includeExported && result.excludedExported > 0 ? `${result.excludedExported} exported symbol(s) excluded (all have callers). Use --include-exported to audit them.` : undefined
1915
+ exportedHint: !iflags.includeExported && result.excludedExported > 0 ? `${result.excludedExported} exported symbol(s) excluded (all have callers). Use --include-exported to audit them.` : undefined,
1916
+ externalContractHint: !iflags.includeExported && result.excludedExternalContract > 0 ? `${result.excludedExternalContract} symbol(s) hidden (override an out-of-tree base class — reachable via external contract, not dead). Use --include-exported to include them.` : undefined
1915
1917
  }));
1916
1918
  break;
1917
1919
  }
package/core/callers.js CHANGED
@@ -10,7 +10,7 @@ const path = require('path');
10
10
  const crypto = require('crypto');
11
11
  const { detectLanguage, getParser, getLanguageModule, langTraits } = require('../languages');
12
12
  const { isTestFile } = require('./discovery');
13
- const { NON_CALLABLE_TYPES } = require('./shared');
13
+ const { NON_CALLABLE_TYPES, isOverrideMarked } = require('./shared');
14
14
  const { scoreEdge, tierForResolution, TIER } = require('./confidence');
15
15
  const { findGoModule } = require('./imports');
16
16
 
@@ -233,7 +233,7 @@ function findCallers(index, name, options = {}) {
233
233
  // `some`, not `every`: one marked overload proves the NAME exists
234
234
  // on an external contract — receiver identity is then unprovable
235
235
  // for every call shape (external signatures are invisible).
236
- const marked = tDefs.find(d => _externalContractMarker(d));
236
+ const marked = tDefs.find(d => isOverrideMarked(d));
237
237
  if (marked) _extContract = { via: _externalContractVia(index, marked) };
238
238
  }
239
239
  return _extContract;
@@ -3838,7 +3838,7 @@ function _nameBindingReaches(index, startAbs, name, targetFiles, maxDepth = 4) {
3838
3838
  const next = [];
3839
3839
  for (const [abs, attr] of frontier) {
3840
3840
  if (targetFiles.has(abs)) return 'yes';
3841
- const stateKey = `${abs}${attr}`;
3841
+ const stateKey = `${abs}\x00${attr}`;
3842
3842
  if (visited.has(stateKey)) continue;
3843
3843
  visited.add(stateKey);
3844
3844
  const fe = index.files.get(abs);
@@ -4463,22 +4463,8 @@ function _targetAncestryFullyResolved(index, targetDefs) {
4463
4463
  return true;
4464
4464
  }
4465
4465
 
4466
- /**
4467
- * Explicit override marker on a method definition (fix #210). Marker fields
4468
- * are language-disjoint: traitImpl is Rust-only, an 'override' modifier is
4469
- * Java's lowercased @Override annotation, an override-bearing memberType is
4470
- * TS's `override` keyword, and an override decorator is Python's
4471
- * typing.@override. The marker is compiler-checked syntax in all four —
4472
- * never inferred.
4473
- */
4474
- function _externalContractMarker(def) {
4475
- if (def.traitImpl) return true;
4476
- if (def.modifiers && def.modifiers.includes('override')) return true;
4477
- if (def.memberType && /\boverride\b/.test(def.memberType)) return true;
4478
- if (def.decorators && def.decorators.some(d =>
4479
- String(d).replace(/\(.*$/, '').split('.').pop() === 'override')) return true;
4480
- return false;
4481
- }
4466
+ // _externalContractMarker moved to core/shared.js as isOverrideMarked (shared
4467
+ // with deadcode's out-of-tree override suppression one source of truth).
4482
4468
 
4483
4469
  /**
4484
4470
  * Name of the external contract a marked method implements, for dispatch
package/core/deadcode.js CHANGED
@@ -8,6 +8,90 @@
8
8
  const { detectLanguage, getParser, getLanguageModule, safeParse, langTraits } = require('../languages');
9
9
  const { isTestFile } = require('./discovery');
10
10
  const { isFrameworkEntrypoint } = require('./entrypoints');
11
+ const { splitParentList } = require('./graph-build');
12
+ const { isOverrideMarked } = require('./shared');
13
+
14
+ const _CLASS_KINDS = ['class', 'struct', 'interface', 'trait', 'record'];
15
+
16
+ /** Strip a base-type expression to its bare name: `Mapping[str, int]`→Mapping, `java.util.List<Foo>`→List, `a::b::C`→C. */
17
+ function _bareBaseName(raw) {
18
+ return String(raw).replace(/[<[(].*$/s, '').split('.').pop().split('::').pop().trim();
19
+ }
20
+
21
+ // The universal object root (Python `object`, Java/JS `Object`) has a fixed,
22
+ // known method surface — Object/dunder methods, themselves entry points — so it
23
+ // never dispatches an arbitrary subclass method by name or override. It is NOT
24
+ // an external *dispatching* base: `class Foo(object)` must still report its
25
+ // genuinely-dead inherent methods, exactly like `class Foo`, instead of
26
+ // diverging on a purely cosmetic base declaration. (Java `Object` is the
27
+ // universalSupertype trait; `object` is the Python equivalent — a language
28
+ // convention, rule #9.)
29
+ const _UNIVERSAL_ROOTS = new Set(['object', 'Object']);
30
+
31
+ /** True when a base name resolves to NO in-project class/struct/interface/trait/record (an out-of-tree type). */
32
+ function _baseIsExternal(index, bare) {
33
+ if (!bare || _UNIVERSAL_ROOTS.has(bare)) return false;
34
+ const defs = index.symbols.get(bare);
35
+ return !(defs && defs.some(d => _CLASS_KINDS.includes(d.type)));
36
+ }
37
+
38
+ /**
39
+ * Does the method's enclosing class EXTEND at least one base that is NOT in the
40
+ * project index? An out-of-tree base is a framework/library type UCN can't see;
41
+ * via inheritance it may dispatch into a public method of the subclass
42
+ * polymorphically (Starlette → build_middleware_stack) or by name convention
43
+ * (Pydantic → bytes_schema). The class def is matched in the method's own file.
44
+ *
45
+ * `implements` is deliberately NOT consulted: implementing an external
46
+ * interface/trait makes only the INTERFACE'S OWN methods a contract (those
47
+ * carry traitImpl/@Override markers → handled by Rule A), not the class's
48
+ * unrelated inherent methods. Counting it would wrongly shield, e.g., a Rust
49
+ * struct's genuinely-dead inherent method just because the struct also
50
+ * `impl Display for`s.
51
+ */
52
+ function _classHasExternalBase(index, symbol) {
53
+ const classDefs = (index.symbols.get(symbol.className) || []).filter(c =>
54
+ c.file === symbol.file && _CLASS_KINDS.includes(c.type));
55
+ for (const cd of classDefs) {
56
+ if (!cd.extends) continue;
57
+ const supers = Array.isArray(cd.extends) ? cd.extends : splitParentList(cd.extends);
58
+ for (const raw of supers) {
59
+ if (_baseIsExternal(index, _bareBaseName(raw))) return true;
60
+ }
61
+ }
62
+ return false;
63
+ }
64
+
65
+ /**
66
+ * A method that may be reached through an out-of-tree base class — the deadcode
67
+ * analog of fix #210's external-contract methods. A zero in-project usage count
68
+ * is NOT evidence of deadness here; claiming the symbol dead invites deleting a
69
+ * live framework override (e.g. FastAPI.build_middleware_stack overriding
70
+ * Starlette, or GenerateJsonSchema.bytes_schema name-dispatched by Pydantic —
71
+ * the only caller lives in an unindexed dependency).
72
+ * (A) explicit override marker (@Override / `override` / typing.@override /
73
+ * Rust `impl Trait for X`) AND a single project-wide method owner of the
74
+ * name (no in-project supertype defines it → the contract is external —
75
+ * the #210 ownerCount===1 rule).
76
+ * (B) a public-by-shape method whose class EXTENDS an unresolved base.
77
+ * Private/underscore members are never external-contract surface, so a
78
+ * genuinely-dead one stays claimable (the fix #211 shape predicate).
79
+ * Data-driven, not language-keyed: classes without an `extends` clause (Go
80
+ * embedding, Rust structs / inherent impls) never trip (B), and Rust trait
81
+ * impls trip (A) via traitImpl — so new languages inherit correct behavior.
82
+ */
83
+ function overridesOutOfTreeBase(index, symbol) {
84
+ if (!symbol.className) return false; // standalone function can't override
85
+ if (isOverrideMarked(symbol)) {
86
+ const owners = (index.symbols.get(symbol.name) || []).filter(s => s.className);
87
+ if (owners.length <= 1) return true;
88
+ }
89
+ const mods = symbol.modifiers || [];
90
+ const publicByShape = !mods.includes('private') &&
91
+ !symbol.name.startsWith('#') && !symbol.name.startsWith('_');
92
+ if (publicByShape && _classHasExternalBase(index, symbol)) return true;
93
+ return false;
94
+ }
11
95
 
12
96
  /** Check if a position in a line is inside a string literal (quotes/backticks) */
13
97
  function isInsideString(line, pos) {
@@ -228,6 +312,7 @@ function deadcode(index, options = {}) {
228
312
  const results = [];
229
313
  let excludedDecorated = 0;
230
314
  let excludedExported = 0;
315
+ let excludedExternalContract = 0;
231
316
 
232
317
  // Ensure callee index is built (lazy, reused across operations)
233
318
  if (!index.calleeIndex) {
@@ -488,6 +573,16 @@ function deadcode(index, options = {}) {
488
573
  const totalUsages = nonDefUsages.length;
489
574
 
490
575
  if (totalUsages === 0) {
576
+ // External-contract override: the method may be invoked through
577
+ // an out-of-tree base class UCN can't index (deadcode analog of
578
+ // fix #210). A zero usage count is not evidence of deadness.
579
+ // Hidden by default; revealed under --include-exported, since
580
+ // it is external-reachable surface, not internal dead code.
581
+ const isExternalContract = overridesOutOfTreeBase(index, symbol);
582
+ if (isExternalContract && !options.includeExported) {
583
+ excludedExternalContract++;
584
+ continue;
585
+ }
491
586
  // Collect decorators/annotations for hint display
492
587
  // Python: symbol.decorators (e.g., ['app.route("/path")', 'login_required'])
493
588
  // Java/Rust/Go: symbol.modifiers may contain annotations (e.g., 'bean', 'scheduled')
@@ -530,7 +625,8 @@ function deadcode(index, options = {}) {
530
625
  usageCount: 0,
531
626
  ...(decorators.length > 0 && { decorators }),
532
627
  ...(annotations.length > 0 && { annotations }),
533
- ...(declaredOn && { declaredOn })
628
+ ...(declaredOn && { declaredOn }),
629
+ ...(isExternalContract && { externalContract: true })
534
630
  });
535
631
  }
536
632
  }
@@ -545,6 +641,7 @@ function deadcode(index, options = {}) {
545
641
  // Attach exclusion counts as array properties (backwards-compatible)
546
642
  results.excludedDecorated = excludedDecorated;
547
643
  results.excludedExported = excludedExported;
644
+ results.excludedExternalContract = excludedExternalContract;
548
645
 
549
646
  return results;
550
647
  } finally { index._endOp(); }
package/core/execute.js CHANGED
@@ -751,9 +751,10 @@ const HANDLERS = {
751
751
  if (limit && limit > 0 && Array.isArray(result) && result.length > limit) {
752
752
  note = limitNote(limit, result.length);
753
753
  const sliced = result.slice(0, limit);
754
- // Preserve custom properties (excludedExported, excludedDecorated) from deadcode()
754
+ // Preserve custom properties (excludedExported, excludedDecorated, excludedExternalContract) from deadcode()
755
755
  if (result.excludedExported != null) sliced.excludedExported = result.excludedExported;
756
756
  if (result.excludedDecorated != null) sliced.excludedDecorated = result.excludedDecorated;
757
+ if (result.excludedExternalContract != null) sliced.excludedExternalContract = result.excludedExternalContract;
757
758
  result = sliced;
758
759
  }
759
760
  const tNote = truncationNote(index);
@@ -263,4 +263,4 @@ function splitParentList(clause) {
263
263
  .filter(Boolean);
264
264
  }
265
265
 
266
- module.exports = { buildDirIndex, buildImportGraph, buildInheritanceGraph, _resolveJavaPackageImport };
266
+ module.exports = { buildDirIndex, buildImportGraph, buildInheritanceGraph, splitParentList, _resolveJavaPackageImport };
@@ -194,7 +194,7 @@ function formatStatsJson(stats) {
194
194
  * @param {string} [options.exportedHint] - Hint about exported symbols exclusion
195
195
  */
196
196
  function formatDeadcode(results, options = {}) {
197
- if (results.length === 0 && !results.excludedDecorated && !results.excludedExported) {
197
+ if (results.length === 0 && !results.excludedDecorated && !results.excludedExported && !results.excludedExternalContract) {
198
198
  return 'No dead code found.';
199
199
  }
200
200
 
@@ -232,7 +232,11 @@ function formatDeadcode(results, options = {}) {
232
232
  const declStr = item.declaredOn
233
233
  ? ` [declared on ${item.declaredOn.kind} ${item.declaredOn.name} — contract surface, not executable code]`
234
234
  : '';
235
- lines.push(` ${lineRange(item.startLine, item.endLine)} ${item.name} (${item.type})${exported}${hintStr}${declStr}`);
235
+ // Revealed under --include-exported: mark as external-reachable, not dead.
236
+ const extStr = item.externalContract
237
+ ? ' [reachable via out-of-tree base — external contract, not dead]'
238
+ : '';
239
+ lines.push(` ${lineRange(item.startLine, item.endLine)} ${item.name} (${item.type})${exported}${hintStr}${declStr}${extStr}`);
236
240
  }
237
241
 
238
242
  if (hidden > 0) {
@@ -251,6 +255,10 @@ function formatDeadcode(results, options = {}) {
251
255
  const exportedHint = options.exportedHint || `${results.excludedExported} exported symbol(s) excluded (all have callers). Use --include-exported to audit them.`;
252
256
  lines.push(`\n${exportedHint}`);
253
257
  }
258
+ if (results.excludedExternalContract > 0) {
259
+ const extHint = options.externalContractHint || `${results.excludedExternalContract} symbol(s) hidden (override an out-of-tree base class — reachable via external contract, not dead). Use --include-exported to include them.`;
260
+ lines.push(`\n${extHint}`);
261
+ }
254
262
 
255
263
  if (lines.length === 0) {
256
264
  return 'No dead code found.';
@@ -270,6 +278,7 @@ function formatDeadcodeJson(results) {
270
278
  count: results.length,
271
279
  ...(results.excludedExported > 0 && { excludedExported: results.excludedExported }),
272
280
  ...(results.excludedDecorated > 0 && { excludedDecorated: results.excludedDecorated }),
281
+ ...(results.excludedExternalContract > 0 && { excludedExternalContract: results.excludedExternalContract }),
273
282
  symbols: results.map(item => {
274
283
  const handleSym = { ...item, relativePath: item.relativePath || item.file };
275
284
  const handle = formatSymbolHandle(handleSym);
@@ -283,7 +292,8 @@ function formatDeadcodeJson(results) {
283
292
  ...(item.isExported && { isExported: true }),
284
293
  ...(item.decorators && item.decorators.length > 0 && { decorators: item.decorators }),
285
294
  ...(item.annotations && item.annotations.length > 0 && { annotations: item.annotations }),
286
- ...(item.declaredOn && { declaredOn: item.declaredOn })
295
+ ...(item.declaredOn && { declaredOn: item.declaredOn }),
296
+ ...(item.externalContract && { externalContract: true })
287
297
  };
288
298
  }),
289
299
  },
package/core/shared.js CHANGED
@@ -139,6 +139,26 @@ function looksLikeHandle(input) {
139
139
  return /^.+:\d+(?::.+)?$/.test(input);
140
140
  }
141
141
 
142
+ /**
143
+ * Explicit override marker on a method definition (fix #210). Marker fields are
144
+ * language-disjoint and compiler-checked syntax (never inferred): traitImpl is
145
+ * Rust's `impl Trait`, an 'override' modifier is Java's lowercased @Override, an
146
+ * override-bearing memberType is TS's `override` keyword, and an override
147
+ * decorator is Python's typing.@override. Shared by the external-contract
148
+ * reasoning in both the caller dispatch gate and deadcode (out-of-tree override
149
+ * suppression) — one source of truth so a new marker is added once, not in two
150
+ * drifting copies.
151
+ */
152
+ function isOverrideMarked(def) {
153
+ if (def.traitImpl) return true;
154
+ const mods = def.modifiers || [];
155
+ if (mods.includes('override')) return true;
156
+ if (def.memberType && /\boverride\b/.test(def.memberType)) return true;
157
+ if (def.decorators && def.decorators.some(d =>
158
+ String(d).replace(/\(.*$/, '').split('.').pop() === 'override')) return true;
159
+ return false;
160
+ }
161
+
142
162
  module.exports = {
143
163
  pickBestDefinition,
144
164
  addTestExclusions,
@@ -148,4 +168,5 @@ module.exports = {
148
168
  parseSymbolHandle,
149
169
  looksLikeHandle,
150
170
  isTestPath,
171
+ isOverrideMarked,
151
172
  };
package/mcp/server.js CHANGED
@@ -576,7 +576,8 @@ server.registerTool(
576
576
  let dcText = output.formatDeadcode(result, {
577
577
  top: ep.top || 0,
578
578
  decoratedHint: !ep.includeDecorated && result.excludedDecorated > 0 ? `${result.excludedDecorated} decorated/annotated symbol(s) hidden (framework-registered). Use include_decorated=true to include them.` : undefined,
579
- exportedHint: !ep.includeExported && result.excludedExported > 0 ? `${result.excludedExported} exported symbol(s) excluded (all have callers). Use include_exported=true to audit them.` : undefined
579
+ exportedHint: !ep.includeExported && result.excludedExported > 0 ? `${result.excludedExported} exported symbol(s) excluded (all have callers). Use include_exported=true to audit them.` : undefined,
580
+ externalContractHint: !ep.includeExported && result.excludedExternalContract > 0 ? `${result.excludedExternalContract} symbol(s) hidden (override an out-of-tree base class — reachable via external contract, not dead). Use include_exported=true to include them.` : undefined
580
581
  });
581
582
  if (dcNote) dcText += '\n\n' + dcNote;
582
583
  return tr(dcText);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucn",
3
- "version": "4.0.0",
3
+ "version": "4.0.1",
4
4
  "mcpName": "io.github.mleoca/ucn",
5
5
  "description": "Code intelligence toolkit for AI agents — extract functions, trace call chains, find callers, detect dead code without reading entire files. Works as MCP server, CLI, or agent skill. Supports JS/TS, Python, Go, Rust, Java.",
6
6
  "main": "index.js",