ucn 4.1.2 → 4.2.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/core/check.js CHANGED
@@ -11,7 +11,33 @@
11
11
 
12
12
  'use strict';
13
13
 
14
- const { diffImpact } = require('./analysis');
14
+ const { diffImpact, composeAccount, callNotResolvedEntries } = require('./analysis');
15
+
16
+ function summarizeAccount(account) {
17
+ if (!account) {
18
+ return {
19
+ available: false,
20
+ textComplete: false,
21
+ semanticComplete: false,
22
+ safeToDelete: false,
23
+ };
24
+ }
25
+ const contract = account.contract || {};
26
+ return {
27
+ available: true,
28
+ conserved: account.conserved === true,
29
+ textComplete: contract.textComplete === true,
30
+ observedTextZero: contract.observedTextZero === true,
31
+ semanticComplete: false,
32
+ safeToDelete: false,
33
+ unparsedFiles: account.unparsed ? account.unparsed.fileCount || 0 : 0,
34
+ unreadableFiles: Array.isArray(account.unreadableFiles) ? account.unreadableFiles.length : 0,
35
+ unaccounted: account.unaccounted || 0,
36
+ filtered: account.filtered ? account.filtered.total || 0 : 0,
37
+ beyondTextCallers: account.beyondText ? account.beyondText.count || 0 : 0,
38
+ requiresUsageReview: contract.requiresUsageReview === true,
39
+ };
40
+ }
15
41
 
16
42
  /**
17
43
  * Run the pre-commit check.
@@ -61,12 +87,11 @@ function check(index, options = {}) {
61
87
  const changed = limit ? allChanged.slice(0, limit) : allChanged;
62
88
 
63
89
  const items = [];
64
- const reachable = computeReachableSet(index);
65
90
 
66
91
  // For each changed function, run verify and gather caller summary
67
92
  for (const fn of changed) {
68
93
  const filePath = fn.relativePath || fn.file || '';
69
- let verifyResult = null;
94
+ let verifyResult;
70
95
  try {
71
96
  verifyResult = index.verify(fn.name, { file: filePath });
72
97
  } catch (e) {
@@ -81,12 +106,22 @@ function check(index, options = {}) {
81
106
  // bands already (confirmed `callers` + visible `unverifiedCallers`).
82
107
  let callers = Array.isArray(fn.callers) ? fn.callers : [];
83
108
  let unverifiedCallers = Array.isArray(fn.unverifiedCallers) ? fn.unverifiedCallers : [];
109
+ let account = fn.account || null;
84
110
  if (callers.length === 0 && fn._kind === 'added') {
85
111
  try {
86
- const raw = index.findCallers(fn.name, { includeMethods: true, collectAccount: true }) || [];
112
+ const definitions = (index.symbols.get(fn.name) || []).filter(d =>
113
+ (d.relativePath === filePath || d.file === fn.filePath) &&
114
+ (!fn.startLine || d.startLine === fn.startLine));
115
+ const raw = index.findCallers(fn.name, {
116
+ includeMethods: true,
117
+ collectAccount: true,
118
+ targetDefinitions: definitions.length > 0 ? definitions : undefined,
119
+ }) || [];
87
120
  callers = raw.filter(c => c.tier !== 'unverified');
88
121
  unverifiedCallers = raw.filter(c => c.tier === 'unverified')
89
122
  .concat(raw.unverifiedEntries || []);
123
+ account = composeAccount(index, fn.name, raw);
124
+ unverifiedCallers = unverifiedCallers.concat(callNotResolvedEntries(index, account));
90
125
  } catch (e) { /* skip */ }
91
126
  }
92
127
 
@@ -98,6 +133,7 @@ function check(index, options = {}) {
98
133
  callerCount: callers.length,
99
134
  unverifiedCallerCount: unverifiedCallers.length,
100
135
  signatureMismatches: mismatches.length,
136
+ account: summarizeAccount(account),
101
137
  ...(mismatches.length > 0 && { mismatches: mismatches.slice(0, 5) }),
102
138
  };
103
139
 
@@ -134,6 +170,13 @@ function check(index, options = {}) {
134
170
  callerCount: 0,
135
171
  unverifiedCallerCount: (d.remainingCallSites || []).length,
136
172
  signatureMismatches: 0,
173
+ account: {
174
+ available: false,
175
+ reason: 'deleted-target-cannot-be-reconciled-against-the-current-index',
176
+ textComplete: false,
177
+ semanticComplete: false,
178
+ safeToDelete: false,
179
+ },
137
180
  });
138
181
  }
139
182
 
@@ -157,6 +200,34 @@ function check(index, options = {}) {
157
200
  // Action items
158
201
  const actions = [];
159
202
  for (const it of items) {
203
+ if (!it.account || !it.account.textComplete) {
204
+ actions.push({
205
+ severity: 'error',
206
+ kind: 'incomplete_account',
207
+ message: `${it.name}: caller evidence is not text-complete; inspect warnings and usages manually`,
208
+ });
209
+ }
210
+ if (it.unverifiedCallerCount > 0) {
211
+ actions.push({
212
+ severity: 'warn',
213
+ kind: 'unverified_callers',
214
+ message: `${it.name}: review ${it.unverifiedCallerCount} unverified caller candidate(s)`,
215
+ });
216
+ }
217
+ if (it.account && it.account.filtered > 0) {
218
+ actions.push({
219
+ severity: 'warn',
220
+ kind: 'filtered_evidence',
221
+ message: `${it.name}: ${it.account.filtered} caller edge(s) were hidden by filters`,
222
+ });
223
+ }
224
+ if (it.account && it.account.requiresUsageReview) {
225
+ actions.push({
226
+ severity: 'warn',
227
+ kind: 'non_call_usages',
228
+ message: `${it.name}: non-call references require a usages review`,
229
+ });
230
+ }
160
231
  if (it.signatureMismatches > 0) {
161
232
  actions.push({
162
233
  severity: 'warn',
@@ -168,10 +239,17 @@ function check(index, options = {}) {
168
239
  actions.push({
169
240
  severity: 'warn',
170
241
  kind: 'orphan_new',
171
- message: `${it.name} is new but has no callers (confirmed or unverified) and is not an entry point`,
242
+ message: `${it.name} is new with no observed caller candidates and is not a detected entry point; review usages before treating it as orphaned`,
172
243
  });
173
244
  }
174
245
  }
246
+ if (limit && allChanged.length > limit) {
247
+ actions.push({
248
+ severity: 'error',
249
+ kind: 'truncated_change_set',
250
+ message: `${allChanged.length - limit} changed function(s) were not checked; rerun without --limit`,
251
+ });
252
+ }
175
253
  if (testFiles.length > 0) {
176
254
  const filesList = testFiles.slice(0, 5).map(t => t.file).join(' ');
177
255
  actions.push({
@@ -181,6 +259,16 @@ function check(index, options = {}) {
181
259
  });
182
260
  }
183
261
 
262
+ const incompleteAccounts = items.filter(it => !it.account || !it.account.textComplete).length;
263
+ const unverifiedCallSites = items.reduce((sum, it) => sum + (it.unverifiedCallerCount || 0), 0);
264
+ const signatureMismatches = items.reduce((sum, it) => sum + (it.signatureMismatches || 0), 0);
265
+ const filteredEdges = items.reduce((sum, it) => sum + (it.account ? it.account.filtered || 0 : 0), 0);
266
+ const usageReviewSymbols = items.filter(it => it.account && it.account.requiresUsageReview).length;
267
+ const reviewRequired = incompleteAccounts > 0 || unverifiedCallSites > 0 ||
268
+ signatureMismatches > 0 || filteredEdges > 0 || usageReviewSymbols > 0 ||
269
+ actions.some(a => a.kind === 'orphan_new') ||
270
+ !!(limit && allChanged.length > limit);
271
+
184
272
  return {
185
273
  base: options.base || 'HEAD',
186
274
  staged: !!options.staged,
@@ -191,21 +279,21 @@ function check(index, options = {}) {
191
279
  totalTestFiles: testFiles.length,
192
280
  totalTests: testCount,
193
281
  actions,
282
+ trust: {
283
+ status: incompleteAccounts > 0 || signatureMismatches > 0
284
+ ? 'BLOCKED'
285
+ : reviewRequired ? 'REVIEW_REQUIRED' : 'READY_FOR_TOOLCHAIN',
286
+ accountsChecked: items.filter(it => it.account && it.account.available).length,
287
+ incompleteAccounts,
288
+ unverifiedCallSites,
289
+ signatureMismatches,
290
+ filteredEdges,
291
+ usageReviewSymbols,
292
+ semanticComplete: false,
293
+ safeToDelete: false,
294
+ requiresCompilerAndTests: true,
295
+ },
194
296
  };
195
297
  }
196
298
 
197
- function symbolKey(file, line) {
198
- return `${file}:${line}`;
199
- }
200
-
201
- function computeReachableSet(index) {
202
- try {
203
- const ep = require('./entrypoints');
204
- if (typeof ep.computeReachability === 'function') {
205
- return ep.computeReachability(index);
206
- }
207
- } catch (e) { /* fall through */ }
208
- return null;
209
- }
210
-
211
299
  module.exports = { check };
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * core/confidence.js - Deterministic edge confidence scoring
3
3
  *
4
- * Assigns a 0.0-1.0 confidence score to each caller/callee edge based on
5
- * how the call was resolved. Rule-based, no ML.
4
+ * Assigns an ordinal 0.0-1.0 evidence weight to each caller/callee edge based
5
+ * on how the call was resolved. Rule-based, no ML. These values rank evidence;
6
+ * they are NOT probabilities and are NOT calibrated accuracy estimates.
6
7
  */
7
8
 
8
9
  'use strict';
@@ -19,7 +20,8 @@ const RESOLUTION = {
19
20
  UNCERTAIN: 'uncertain',
20
21
  };
21
22
 
22
- // Seed scores per resolution type (tunable)
23
+ // Ordinal evidence weights per resolution type. The legacy public field is
24
+ // named `confidence` for compatibility; do not interpret 0.98 as "98% likely".
23
25
  const SCORES = {
24
26
  [RESOLUTION.EXACT_BINDING]: 0.98,
25
27
  [RESOLUTION.SAME_CLASS]: 0.92,
@@ -59,6 +61,17 @@ function tierForResolution(resolution) {
59
61
  return RESOLUTION_TIER[resolution] || TIER.UNVERIFIED;
60
62
  }
61
63
 
64
+ function scored(resolution, reasons) {
65
+ const evidenceScore = SCORES[resolution];
66
+ return {
67
+ confidence: evidenceScore,
68
+ evidenceScore,
69
+ scoreKind: 'ordinal-evidence-not-probability',
70
+ resolution,
71
+ evidence: reasons,
72
+ };
73
+ }
74
+
62
75
  /**
63
76
  * Score a caller/callee edge based on resolution evidence.
64
77
  *
@@ -71,7 +84,7 @@ function tierForResolution(resolution) {
71
84
  * @param {boolean} [evidence.isUncertain] - Marked uncertain by resolution logic
72
85
  * @param {boolean} [evidence.isFunctionReference] - Passed as callback argument
73
86
  * @param {boolean} [evidence.hasReceiverType] - Go/Java/Rust parser-inferred receiverType
74
- * @returns {{ confidence: number, resolution: string, evidence: string[] }}
87
+ * @returns {{ confidence: number, evidenceScore: number, scoreKind: string, resolution: string, evidence: string[] }}
75
88
  */
76
89
  function scoreEdge(evidence) {
77
90
  const reasons = [];
@@ -81,38 +94,38 @@ function scoreEdge(evidence) {
81
94
  // (without this, a known mismatch would score receiver-hint 0.80).
82
95
  if (evidence.typeMismatch) {
83
96
  reasons.push('receiver type mismatch');
84
- return { confidence: SCORES[RESOLUTION.UNCERTAIN], resolution: RESOLUTION.UNCERTAIN, evidence: reasons };
97
+ return scored(RESOLUTION.UNCERTAIN, reasons);
85
98
  }
86
99
 
87
100
  // Nominal dispatch tiering (contract surface only — callers.js sets these
88
101
  // flags exclusively under collectAccount, so legacy paths never see them).
89
102
  if (evidence.possibleDispatch) {
90
103
  reasons.push('interface/supertype dispatch');
91
- return { confidence: SCORES[RESOLUTION.POSSIBLE_DISPATCH], resolution: RESOLUTION.POSSIBLE_DISPATCH, evidence: reasons };
104
+ return scored(RESOLUTION.POSSIBLE_DISPATCH, reasons);
92
105
  }
93
106
  if (evidence.methodAmbiguous) {
94
107
  reasons.push('untyped receiver, multiple same-name definitions');
95
- return { confidence: SCORES[RESOLUTION.METHOD_AMBIGUOUS], resolution: RESOLUTION.METHOD_AMBIGUOUS, evidence: reasons };
108
+ return scored(RESOLUTION.METHOD_AMBIGUOUS, reasons);
96
109
  }
97
110
 
98
111
  // Exact binding match (highest confidence)
99
112
  if (evidence.hasBindingId) {
100
113
  reasons.push('binding-id match');
101
114
  if (evidence.hasImportEvidence) reasons.push('import-verified');
102
- return { confidence: SCORES[RESOLUTION.EXACT_BINDING], resolution: RESOLUTION.EXACT_BINDING, evidence: reasons };
115
+ return scored(RESOLUTION.EXACT_BINDING, reasons);
103
116
  }
104
117
 
105
118
  // Same-class resolution (self/this/super/cls)
106
119
  if (evidence.resolvedBySameClass) {
107
120
  reasons.push('same-class method');
108
121
  if (evidence.hasInheritanceChain) reasons.push('via inheritance');
109
- return { confidence: SCORES[RESOLUTION.SAME_CLASS], resolution: RESOLUTION.SAME_CLASS, evidence: reasons };
122
+ return scored(RESOLUTION.SAME_CLASS, reasons);
110
123
  }
111
124
 
112
125
  // Receiver hint narrowed to specific type
113
126
  if (evidence.resolvedByReceiverHint || evidence.hasReceiverType) {
114
127
  reasons.push(evidence.hasReceiverType ? 'parser receiver-type' : 'local type inference');
115
- return { confidence: SCORES[RESOLUTION.RECEIVER_HINT], resolution: RESOLUTION.RECEIVER_HINT, evidence: reasons };
128
+ return scored(RESOLUTION.RECEIVER_HINT, reasons);
116
129
  }
117
130
 
118
131
  // Function reference (callback / passed-as-argument). Argument position is
@@ -123,10 +136,10 @@ function scoreEdge(evidence) {
123
136
  reasons.push('function reference');
124
137
  if (evidence.hasImportEvidence || evidence.hasSamePackageEvidence) {
125
138
  reasons.push(evidence.hasImportEvidence ? 'import-supported' : 'same package/module');
126
- return { confidence: SCORES[RESOLUTION.SCOPE_MATCH], resolution: RESOLUTION.SCOPE_MATCH, evidence: reasons };
139
+ return scored(RESOLUTION.SCOPE_MATCH, reasons);
127
140
  }
128
141
  reasons.push('no import evidence');
129
- return { confidence: SCORES[RESOLUTION.NAME_ONLY], resolution: RESOLUTION.NAME_ONLY, evidence: reasons };
142
+ return scored(RESOLUTION.NAME_ONLY, reasons);
130
143
  }
131
144
 
132
145
  // Scope/import-supported match
@@ -134,22 +147,22 @@ function scoreEdge(evidence) {
134
147
  if (evidence.hasImportEvidence) reasons.push('import-supported');
135
148
  if (evidence.hasReceiverEvidence) reasons.push('receiver binding in scope');
136
149
  if (evidence.hasSamePackageEvidence) reasons.push('same package/module');
137
- return { confidence: SCORES[RESOLUTION.SCOPE_MATCH], resolution: RESOLUTION.SCOPE_MATCH, evidence: reasons };
150
+ return scored(RESOLUTION.SCOPE_MATCH, reasons);
138
151
  }
139
152
 
140
153
  // Uncertain
141
154
  if (evidence.isUncertain) {
142
155
  reasons.push('ambiguous resolution');
143
- return { confidence: SCORES[RESOLUTION.UNCERTAIN], resolution: RESOLUTION.UNCERTAIN, evidence: reasons };
156
+ return scored(RESOLUTION.UNCERTAIN, reasons);
144
157
  }
145
158
 
146
159
  // Name-only match (no additional evidence)
147
160
  reasons.push('name match only');
148
- return { confidence: SCORES[RESOLUTION.NAME_ONLY], resolution: RESOLUTION.NAME_ONLY, evidence: reasons };
161
+ return scored(RESOLUTION.NAME_ONLY, reasons);
149
162
  }
150
163
 
151
164
  /**
152
- * Filter edges by minimum confidence threshold.
165
+ * Filter edges by minimum ordinal evidence threshold (legacy name retained).
153
166
  * @param {Array} edges - Array of objects with .confidence property
154
167
  * @param {number} minConfidence - Minimum confidence (0.0-1.0)
155
168
  * @returns {{ kept: Array, filtered: number }}
package/core/deadcode.js CHANGED
@@ -53,32 +53,90 @@ function _baseIsExternal(index, bare, lang) {
53
53
  return !(defs && defs.some(d => _CLASS_KINDS.includes(d.type)));
54
54
  }
55
55
 
56
- /** Does this CLASS DEF extend at least one base that is not in the project index? */
57
- function _classDefHasExternalBase(index, classDef, lang) {
58
- if (!classDef.extends) return false;
59
- const supers = Array.isArray(classDef.extends) ? classDef.extends : splitParentList(classDef.extends);
60
- return supers.some(raw => _baseIsExternal(index, _bareBaseName(raw), lang));
61
- }
56
+ // Bounded heritage-closure depth (fix #270) matches the engine's other
57
+ // inheritance walks.
58
+ const _HERITAGE_WALK_DEPTH = 8;
62
59
 
63
60
  /**
64
- * Does the method's enclosing class EXTEND at least one base that is NOT in the
65
- * project index? An out-of-tree base is a framework/library type UCN can't see;
66
- * via inheritance it may dispatch into a public method of the subclass
67
- * polymorphically (Starlette → build_middleware_stack) or by name convention
68
- * (Pydantic → bytes_schema). The class def is matched in the method's own file.
61
+ * Does this CLASS DEF reach at least one base that is NOT in the project
62
+ * index, walking `extends` chains transitively THROUGH resolved project types
63
+ * (fix #270)? One level was not enough fastify-measured: CustomLoggerImpl
64
+ * implements CustomLogger (project) extends FastifyBaseLogger (project,
65
+ * .d.ts) extends Pick<BaseLogger, ...'silent'> (pino external). The member
66
+ * surface the class must provide comes from the OUT-OF-TREE end of the chain,
67
+ * invisible without tsc, so a zero-usage impl member (`silent`) is contract
68
+ * surface — deleting it breaks the build. Same dispatch physics as one level;
69
+ * only the verdict moved from "direct parent external" to "heritage closure
70
+ * reaches external".
69
71
  *
70
- * `implements` is deliberately NOT consulted: implementing an external
71
- * interface/trait makes only the INTERFACE'S OWN methods a contract (those
72
- * carry traitImpl/@Override markers handled by Rule A), not the class's
73
- * unrelated inherent methods. Counting it would wrongly shield, e.g., a Rust
74
- * struct's genuinely-dead inherent method just because the struct also
75
- * `impl Display for`s.
72
+ * `followImplements` additionally walks the def's OWN `implements` clause
73
+ * (first hop only the measured family; deeper implements hops are
74
+ * classified-deferred). Method claims pass true only when the member sits
75
+ * INSIDE the def's source range: TS/Java members live in the class body, so
76
+ * the implements contract constrains them. Rust surfaces trait impls as
77
+ * `implements` on the struct (rust.js), but inherent methods live in separate
78
+ * `impl X` blocks OUTSIDE the struct's range — an unrelated `impl Display
79
+ * for X` must never shield a genuinely-dead inherent method (the original
80
+ * reason implements was not consulted here at all; trait-impl members
81
+ * themselves carry traitImpl/@Override markers → Rule A). Class-kind claims
82
+ * pass false: deleting the whole class removes its implements clause with it.
83
+ */
84
+ function _heritageReachesExternalBase(index, classDef, lang, followImplements) {
85
+ const seen = new Set();
86
+ let frontier = [classDef];
87
+ for (let depth = 0; depth < _HERITAGE_WALK_DEPTH && frontier.length; depth++) {
88
+ const next = [];
89
+ for (const def of frontier) {
90
+ // Copy array-shaped extends — the implements push below must
91
+ // never mutate the symbol's own heritage data in the index.
92
+ const parents = def.extends
93
+ ? (Array.isArray(def.extends) ? [...def.extends] : splitParentList(def.extends))
94
+ : [];
95
+ if (depth === 0 && followImplements && Array.isArray(def.implements)) {
96
+ parents.push(...def.implements);
97
+ }
98
+ for (const raw of parents) {
99
+ const bare = _bareBaseName(raw);
100
+ if (!bare || seen.has(bare)) continue;
101
+ seen.add(bare);
102
+ if (_baseIsExternal(index, bare, lang)) return true;
103
+ for (const pd of index.symbols.get(bare) || []) {
104
+ if (_CLASS_KINDS.includes(pd.type)) next.push(pd);
105
+ }
106
+ }
107
+ }
108
+ frontier = next;
109
+ }
110
+ return false;
111
+ }
112
+
113
+ /**
114
+ * Does the method's enclosing class reach at least one base that is NOT in
115
+ * the project index through its heritage closure? An out-of-tree base is a
116
+ * framework/library type UCN can't see; via inheritance it may dispatch into
117
+ * a public method of the subclass polymorphically (Starlette →
118
+ * build_middleware_stack), by name convention (Pydantic → bytes_schema), or
119
+ * REQUIRE the member outright (fastify → CustomLoggerImpl.silent, via an
120
+ * implements chain ending in pino). The class def is matched in the method's
121
+ * own file; `implements` is walked only when the member sits inside the class
122
+ * def's own range (see _heritageReachesExternalBase — the Rust guard).
76
123
  */
77
124
  function _classHasExternalBase(index, symbol) {
78
125
  const lang = index.files.get(symbol.file)?.language;
126
+ // The implements hop shields only contract-SATISFIABLE members: an
127
+ // interface implementation must be public, so in explicit-visibility
128
+ // languages a package-private/non-pub member provably cannot implement
129
+ // any interface member (javac rejects it) — `implements Runnable` never
130
+ // shields a package-private helper. Implicit-public languages satisfy
131
+ // contracts with unmarked members; the caller's public-by-shape check
132
+ // already screened those. Compiler physics, not a heuristic.
133
+ const contractSatisfiable = langTraits(lang)?.implicitlyPublicMembers ||
134
+ (symbol.modifiers || []).includes('public');
79
135
  const classDefs = (index.symbols.get(symbol.className) || []).filter(c =>
80
136
  c.file === symbol.file && _CLASS_KINDS.includes(c.type));
81
- return classDefs.some(cd => _classDefHasExternalBase(index, cd, lang));
137
+ return classDefs.some(cd => _heritageReachesExternalBase(index, cd, lang,
138
+ contractSatisfiable &&
139
+ symbol.startLine >= cd.startLine && symbol.startLine <= cd.endLine));
82
140
  }
83
141
 
84
142
  /**
@@ -92,9 +150,12 @@ function _classHasExternalBase(index, symbol) {
92
150
  * Rust `impl Trait for X`) AND a single project-wide method owner of the
93
151
  * name (no in-project supertype defines it → the contract is external —
94
152
  * the #210 ownerCount===1 rule).
95
- * (B) a public-by-shape method whose class EXTENDS an unresolved base.
96
- * Private/underscore members are never external-contract surface, so a
97
- * genuinely-dead one stays claimable (the fix #211 shape predicate).
153
+ * (B) a public-by-shape method whose class reaches an unresolved base
154
+ * through its heritage closure (extends chains walked transitively,
155
+ * plus the class's own `implements` clause when the member sits in the
156
+ * class body — fix #270). Private/underscore members are never
157
+ * external-contract surface, so a genuinely-dead one stays claimable
158
+ * (the fix #211 shape predicate).
98
159
  * Data-driven, not language-keyed: classes without an `extends` clause (Go
99
160
  * embedding, Rust structs / inherent impls) never trip (B), and Rust trait
100
161
  * impls trip (A) via traitImpl — so new languages inherit correct behavior.
@@ -147,8 +208,7 @@ function nameOnlySelfRecursive(index, name) {
147
208
  }
148
209
  const { getCachedCalls } = require('./callers');
149
210
  for (const f of files) {
150
- let calls;
151
- try { calls = getCachedCalls(index, f); } catch { return false; }
211
+ const calls = getCachedCalls(index, f);
152
212
  if (!calls) return false;
153
213
  for (const call of calls) {
154
214
  if (call.name !== name && call.resolvedName !== name &&
@@ -950,7 +1010,7 @@ function deadcode(index, options = {}) {
950
1010
  // by module path, pytest plugins by registration) — zero
951
1011
  // in-project references is not evidence of deadness there.
952
1012
  const isExternalContract = classAuditSet.has(symbol.type)
953
- ? _classDefHasExternalBase(index, symbol, lang)
1013
+ ? _heritageReachesExternalBase(index, symbol, lang, false)
954
1014
  : overridesOutOfTreeBase(index, symbol);
955
1015
  if (isExternalContract && !options.includeExported) {
956
1016
  excludedExternalContract++;
@@ -943,29 +943,27 @@ function detectEntrypoints(index, options = {}) {
943
943
  // (3) Top-level invocation targets: any non-method call with no
944
944
  // enclosingFunction is module-load-time, so its callee is reachable.
945
945
  // We treat the callee as an entry point (the function being kicked off).
946
- try {
947
- const calls = getCachedCalls(index, filePath);
948
- if (calls && calls.length > 0) {
949
- for (const c of calls) {
950
- if (c.enclosingFunction != null) continue;
951
- if (c.isMethod) continue;
952
- // Resolve callee names (handles aliased imports)
953
- const names = c.resolvedNames || (c.resolvedName ? [c.resolvedName] : [c.name]);
954
- for (const n of names) {
955
- if (index.symbols.has(n)) allowed.add(n);
956
- }
946
+ const calls = getCachedCalls(index, filePath);
947
+ if (calls && calls.length > 0) {
948
+ for (const c of calls) {
949
+ if (c.enclosingFunction != null) continue;
950
+ if (c.isMethod) continue;
951
+ // Resolve callee names (handles aliased imports)
952
+ const names = c.resolvedNames || (c.resolvedName ? [c.resolvedName] : [c.name]);
953
+ for (const n of names) {
954
+ if (index.symbols.has(n)) allowed.add(n);
957
955
  }
958
-
959
- // (4) Function containing a top-level `if (require.main === module) { ... }`
960
- // wrapping calls — captured by treating any non-method call whose
961
- // enclosingFunction body is at top level. The simplest detection:
962
- // scan calls inside any function whose body contains the require.main
963
- // guard. We approximate via the same `enclosingFunction` data: if a
964
- // function contains top-level invocation-style entries, it's typically
965
- // identified by the (3) check above. Skip explicit (4) detection here —
966
- // (3) already covers `if (require.main === module) { main(); }`.
967
956
  }
968
- } catch (_e) { /* best-effort */ }
957
+
958
+ // (4) Function containing a top-level `if (require.main === module) { ... }`
959
+ // wrapping calls — captured by treating any non-method call whose
960
+ // enclosingFunction body is at top level. The simplest detection:
961
+ // scan calls inside any function whose body contains the require.main
962
+ // guard. We approximate via the same `enclosingFunction` data: if a
963
+ // function contains top-level invocation-style entries, it's typically
964
+ // identified by the (3) check above. Skip explicit (4) detection here —
965
+ // (3) already covers `if (require.main === module) { main(); }`.
966
+ }
969
967
 
970
968
  // If we identified at least one specific entry, use that set.
971
969
  // Otherwise fall back to permissive (null) so a CLI file with neither
@@ -1143,6 +1141,18 @@ function computeReachability(index) {
1143
1141
  const langModuleCache = new Map();
1144
1142
  for (const [, symbols] of index.symbols) {
1145
1143
  for (const symbol of symbols) {
1144
+ // JS/TS module-scope object registries are invoked through dynamic
1145
+ // property access (HANDLERS[command](...)). There may be no static
1146
+ // incoming edge to discover. Treat these members as conservative
1147
+ // roots so deadcode never presents a dynamically reachable handler
1148
+ // as safe to remove; callees then flow through the normal BFS.
1149
+ if (symbol.registryMember) {
1150
+ const registryKey = symbolKey(symbol.file, symbol.startLine);
1151
+ if (!reachable.has(registryKey)) {
1152
+ reachable.add(registryKey);
1153
+ queue.push(symbol);
1154
+ }
1155
+ }
1146
1156
  const fileEntry = index.files.get(symbol.file);
1147
1157
  if (!fileEntry) continue;
1148
1158
  const lang = fileEntry.language;
@@ -1183,12 +1193,7 @@ function computeReachability(index) {
1183
1193
  for (const [filePath, fileEntry] of index.files) {
1184
1194
  const lang = fileEntry.language;
1185
1195
  if (lang !== 'javascript' && lang !== 'typescript' && lang !== 'tsx') continue;
1186
- let calls;
1187
- try {
1188
- calls = getCachedCalls(index, filePath);
1189
- } catch (_e) {
1190
- continue;
1191
- }
1196
+ const calls = getCachedCalls(index, filePath);
1192
1197
  if (!calls || calls.length === 0) continue;
1193
1198
  for (const call of calls) {
1194
1199
  // Top-level call: AST recorded enclosingFunction === null/undefined.
package/core/execute.js CHANGED
@@ -241,7 +241,6 @@ function checkFilePatternMatch(index, filePattern) {
241
241
  if (fileEntry.relativePath.includes(filePattern)) return null;
242
242
  }
243
243
  // Suggest similar directories/files to help user refine
244
- const patternLower = filePattern.toLowerCase();
245
244
  const basename = filePattern.split('/').pop().toLowerCase();
246
245
  const suggestions = new Set();
247
246
  for (const [, fileEntry] of index.files) {
@@ -493,6 +492,7 @@ const HANDLERS = {
493
492
  example: (index, p) => {
494
493
  const err = requireName(p.name);
495
494
  if (err) return { ok: false, error: err };
495
+ const targetWasHandle = looksLikeHandle(p.name);
496
496
  applyClassMethodSyntax(p);
497
497
  const fileErr = checkFilePatternMatch(index, p.file);
498
498
  if (fileErr) return { ok: false, error: fileErr };
@@ -501,6 +501,12 @@ const HANDLERS = {
501
501
  const result = index.example(p.name, {
502
502
  file: p.file,
503
503
  className: p.className,
504
+ line: p.line,
505
+ // Preserve legacy plain-name `--file` as a call-site scope while
506
+ // letting a stable handle use its file component purely as exact
507
+ // target identity. Without this distinction, handles could never
508
+ // find examples outside the definition file.
509
+ callSiteFile: !targetWasHandle ? p.file : null,
504
510
  diverse: !!p.diverse,
505
511
  top: num(p.top, undefined),
506
512
  // MEDIUM-8: thread includeTests so test-file callers are included
@@ -583,7 +589,6 @@ const HANDLERS = {
583
589
  in: p.in,
584
590
  file: p.file,
585
591
  deep: !!p.deep,
586
- sampleSize: num(p.limit, undefined),
587
592
  });
588
593
  return { ok: true, result };
589
594
  },
@@ -832,10 +837,21 @@ const HANDLERS = {
832
837
  // splitting would shear the filename at the dot into
833
838
  // className='helper', name='go' and silently return nothing
834
839
  // (fix #239, G3-go-measured). Mirror the engine's file-path test.
835
- const testsTargetIsFile = typeof p.name === 'string' && (
840
+ const testsTargetIsHandle = looksLikeHandle(p.name);
841
+ const testsTargetIsFile = !testsTargetIsHandle && typeof p.name === 'string' && (
836
842
  p.name.includes('/') || p.name.includes('\\') ||
837
843
  /\.(js|ts|py|go|java|rs)$/.test(p.name));
838
844
  if (!testsTargetIsFile) applyClassMethodSyntax(p);
845
+ if (testsTargetIsHandle && p.line) {
846
+ const line = Number(p.line);
847
+ const pinned = (index.symbols.get(p.name) || []).filter(d =>
848
+ d.startLine === line && (!p.file || d.relativePath?.includes(p.file)));
849
+ if (pinned.length === 1 && !p.className && pinned[0].className) {
850
+ p.className = pinned[0].className;
851
+ }
852
+ const pinErr = checkDefinitionPin(index, p);
853
+ if (pinErr) return { ok: false, error: pinErr };
854
+ }
839
855
  if (p.file) {
840
856
  const fileErr = checkFilePatternMatch(index, p.file);
841
857
  if (fileErr) return { ok: false, error: fileErr };