ucn 5.5.3 → 5.6.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 (77) hide show
  1. package/.claude/skills/ucn/SKILL.md +37 -12
  2. package/.claude/skills/ucn/references/commands.md +18 -14
  3. package/.claude/skills/ucn/references/resolution-rules.md +4 -0
  4. package/.claude/skills/ucn/references/trust-contract.md +4 -4
  5. package/CHANGELOG.md +485 -0
  6. package/README.md +66 -29
  7. package/cli/flags.js +184 -0
  8. package/cli/index.js +137 -324
  9. package/core/account.js +44 -87
  10. package/core/analysis.js +274 -108
  11. package/core/ast-analysis.js +2 -0
  12. package/core/bridge.js +14 -0
  13. package/core/cache.js +103 -16
  14. package/core/call-fit.js +130 -0
  15. package/core/callers.js +353 -71
  16. package/core/capture.js +363 -0
  17. package/core/change-kinds.js +1107 -0
  18. package/core/check.js +1150 -139
  19. package/core/command-contracts.js +8 -7
  20. package/core/constructed-class.js +1195 -0
  21. package/core/contract-membership.js +142 -0
  22. package/core/deadcode.js +455 -106
  23. package/core/deletion.js +421 -0
  24. package/core/discovery.js +104 -9
  25. package/core/entrypoint-toml.js +110 -0
  26. package/core/entrypoints.js +12 -6
  27. package/core/evidence-bands.js +68 -0
  28. package/core/evidence-bands.json +44 -0
  29. package/core/execute.js +172 -161
  30. package/core/hot-summary.js +241 -0
  31. package/core/index-ir.js +5 -3
  32. package/core/ir.js +6 -2
  33. package/core/member-signatures.js +789 -0
  34. package/core/outgoing-check.js +116 -0
  35. package/core/output/analysis-ext.js +20 -10
  36. package/core/output/analysis.js +98 -60
  37. package/core/output/check.js +101 -37
  38. package/core/output/doctor.js +34 -1
  39. package/core/output/find.js +2 -1
  40. package/core/output/lines.js +63 -3
  41. package/core/output/public.js +29 -11
  42. package/core/output/refactoring.js +32 -0
  43. package/core/output/reporting.js +19 -2
  44. package/core/output/search.js +14 -2
  45. package/core/output/tracing.js +36 -8
  46. package/core/output-budget.js +146 -61
  47. package/core/parallel-build.js +3 -3
  48. package/core/project.js +106 -23
  49. package/core/provenance-type.js +12 -5
  50. package/core/public-command.js +35 -9
  51. package/core/python-binding-identity.js +707 -0
  52. package/core/python-modules.js +6 -0
  53. package/core/reflection.js +248 -31
  54. package/core/registry.js +623 -72
  55. package/core/reporting.js +69 -34
  56. package/core/review-items.js +75 -0
  57. package/core/runtime-entrypoints.js +159 -0
  58. package/core/search.js +148 -164
  59. package/core/shared.js +0 -45
  60. package/core/snapshot.js +350 -0
  61. package/core/source-universe.js +707 -0
  62. package/core/test-units.js +3698 -0
  63. package/core/tracing.js +270 -658
  64. package/core/verify.js +63 -22
  65. package/languages/adapter.js +8 -3
  66. package/languages/c-family.js +12 -0
  67. package/languages/csharp.js +634 -0
  68. package/languages/go.js +120 -0
  69. package/languages/index.js +466 -4
  70. package/languages/java.js +712 -0
  71. package/languages/javascript.js +1498 -6
  72. package/languages/lexical-scope.js +68 -0
  73. package/languages/python.js +1708 -3
  74. package/languages/rust.js +209 -104
  75. package/languages/static-js.js +88 -0
  76. package/mcp/server.js +44 -110
  77. package/package.json +11 -5
package/core/deadcode.js CHANGED
@@ -15,7 +15,13 @@ const { splitParentList } = require('./graph-build');
15
15
  const { isOverrideMarked, codeUnitCompare, lineInRanges, maskBlockComments, escapeRegExp } = require('./shared');
16
16
  const { projectComputedDispatch, projectReflectionSites } = require('./ast-analysis');
17
17
  const { reflectionPatternReferences, reflectionLiteralNames } = require('./reflection');
18
- const { protocolMemberOf, protocolTypeOf, typedLiteralContract, rustProjectTraits } = require('./contract-membership');
18
+ const { protocolMemberOf, protocolTypeOf, typedLiteralContract, rustProjectTraits,
19
+ hierarchySlotClosure, goContractClosure, structuralContractsRequiring,
20
+ requirementIn, resetContractIndex } = require('./contract-membership');
21
+ const { projectKind, downstreamExposureBlocks } = require('./deletion');
22
+ const { pythonIdentity, registrationUnknown, pythonSourceName, pythonNormalizedNames,
23
+ pythonProjectIdentityAffected } = require('./python-binding-identity');
24
+ const { runtimeEntrypoints, runtimeTargetReaches } = require('./runtime-entrypoints');
19
25
 
20
26
  const _CLASS_KINDS = ['class', 'struct', 'interface', 'trait', 'record'];
21
27
 
@@ -29,12 +35,6 @@ const CLASS_AUDIT_KINDS = ['class', 'struct', 'interface', 'trait', 'record', 'e
29
35
  // the callable with an external runtime. They therefore remain eligible for a
30
36
  // name/usage-based dead-code proof. Every other unknown decorator/annotation
31
37
  // stays conservative: it may be the only evidence of framework registration.
32
- const _NON_REGISTERING_DECORATORS = new Set([
33
- 'property',
34
- 'classmethod',
35
- 'staticmethod',
36
- ]);
37
-
38
38
  function _decoratorName(value) {
39
39
  const raw = typeof value === 'string'
40
40
  ? value
@@ -43,7 +43,33 @@ function _decoratorName(value) {
43
43
  return String(raw).trim().replace(/^@/, '').split('(')[0].trim();
44
44
  }
45
45
 
46
- function hasUnknownRegistrationDecorator(symbol) {
46
+ const PYTHON_DECLARATIONS = new WeakMap();
47
+
48
+ function pythonDeclaration(index, symbol) {
49
+ try {
50
+ const operation = index._opTreeCache;
51
+ let byFile = operation && PYTHON_DECLARATIONS.get(operation);
52
+ if (!byFile) {
53
+ byFile = new Map();
54
+ if (operation) PYTHON_DECLARATIONS.set(operation, byFile);
55
+ }
56
+ let byName = byFile.get(symbol.file);
57
+ if (!byName) {
58
+ byName = new Map();
59
+ const tree = index._getParsedTree(symbol.file, index._readFile(symbol.file), 'python');
60
+ for (const node of tree.rootNode.descendantsOfType(['function_definition', 'class_definition'])) {
61
+ const name = node.childForFieldName('name')?.text;
62
+ if (!byName.has(name)) byName.set(name, []);
63
+ byName.get(name).push(node);
64
+ }
65
+ byFile.set(symbol.file, byName);
66
+ }
67
+ return (byName.get(symbol.name) || []).find(node =>
68
+ node.startPosition.row + 1 >= symbol.startLine && node.endPosition.row + 1 <= symbol.endLine);
69
+ } catch { return null; }
70
+ }
71
+
72
+ function hasUnknownRegistrationDecorator(symbol, index) {
47
73
  const values = [
48
74
  symbol.decorators,
49
75
  symbol.decoratorsWithArgs,
@@ -51,15 +77,15 @@ function hasUnknownRegistrationDecorator(symbol) {
51
77
  symbol.attributesWithArgs,
52
78
  ].flatMap(items => Array.isArray(items) ? items : []);
53
79
 
80
+ if (values.length === 0) return false;
81
+ if (index?.files.get(symbol.file)?.language === 'python') {
82
+ if (typeof symbol.registrationUnknown === 'boolean') return symbol.registrationUnknown || pythonProjectIdentityAffected(index, 'builtins.*');
83
+ const declaration = pythonDeclaration(index, symbol);
84
+ return declaration ? registrationUnknown(declaration) || pythonProjectIdentityAffected(index, 'builtins.*') : true;
85
+ }
54
86
  return values.some(value => {
55
87
  const qualified = _decoratorName(value);
56
88
  if (!qualified) return false;
57
- const bare = qualified.split(/\.|::/).pop();
58
- if (_NON_REGISTERING_DECORATORS.has(bare)) return false;
59
- // `@name.getter`, `@name.setter`, and `@name.deleter` install an
60
- // accessor on an already local property; they are not external
61
- // framework registration boundaries.
62
- if (['getter', 'setter', 'deleter'].includes(bare)) return false;
63
89
  return true;
64
90
  });
65
91
  }
@@ -90,9 +116,20 @@ const _UNIVERSAL_ROOTS = new Set(['object', 'Object']);
90
116
  const _PY_NON_DISPATCHING_BASES = new Set(['Generic']);
91
117
 
92
118
  /** True when a base name resolves to NO in-project class/struct/interface/trait/record (an out-of-tree type). */
93
- function _baseIsExternal(index, bare, lang, fromDef) {
94
- if (!bare || _UNIVERSAL_ROOTS.has(bare)) return false;
95
- if (lang === 'python' && _PY_NON_DISPATCHING_BASES.has(bare)) return false;
119
+ function _baseIsExternal(index, bare, lang, fromDef, raw) {
120
+ if (!bare) return false;
121
+ if (lang === 'python' && fromDef) {
122
+ const declaration = pythonDeclaration(index, fromDef);
123
+ let base = declaration?.childForFieldName('superclasses')?.namedChildren.find(node =>
124
+ node.text === raw || (node.type === 'subscript' && node.childForFieldName('value')?.text === raw));
125
+ while (base?.type === 'subscript') base = base.childForFieldName('value');
126
+ const identity = pythonIdentity(base);
127
+ if (identity === 'builtins.object' || identity === 'typing.Generic') return pythonProjectIdentityAffected(index, identity);
128
+ // A source import can reuse a builtin's spelling without its semantics.
129
+ if (identity && (_UNIVERSAL_ROOTS.has(bare) || _PY_NON_DISPATCHING_BASES.has(bare))) return true;
130
+ } else if (_UNIVERSAL_ROOTS.has(bare) &&
131
+ !(index.files.get(fromDef?.file)?.importBindings || []).some(binding => (binding.alias || binding.name) === bare) &&
132
+ !(index.symbols.get(bare) || []).some(def => def.file === fromDef?.file)) return false;
96
133
  const defs = index.symbols.get(bare);
97
134
  return !(defs && defs.some(d => _CLASS_KINDS.includes(d.type) &&
98
135
  // `class EnvironBuilder(werkzeug.test.EnvironBuilder)` must not
@@ -149,7 +186,7 @@ function _heritageReachesExternalBase(index, classDef, lang, followImplements) {
149
186
  const bare = _bareBaseName(raw);
150
187
  if (!bare || seen.has(bare)) continue;
151
188
  seen.add(bare);
152
- if (_baseIsExternal(index, bare, lang, def)) return true;
189
+ if (_baseIsExternal(index, bare, lang, def, raw)) return true;
153
190
  for (const pd of index.symbols.get(bare) || []) {
154
191
  if (_CLASS_KINDS.includes(pd.type)) next.push(pd);
155
192
  }
@@ -245,27 +282,170 @@ function typedLiteralExternalMember(index, symbol) {
245
282
  return !!typedLiteralContract(index, symbol)?.external;
246
283
  }
247
284
 
248
- // Go satisfies interfaces implicitly, so no `implements` edge exists for
249
- // compiler/stdlib callbacks. A zero textual usage for one of these exact
250
- // method shapes is not evidence of deadness (sort.Interface, http.Handler,
251
- // io.Reader, encoding marshalers, database/sql contracts, errors helpers).
252
- const GO_PROTOCOL_METHODS = new Map([
253
- ['Len', 0], ['Less', 2], ['Swap', 2], ['ServeHTTP', 2],
254
- ['Error', 0], ['String', 0], ['Read', 1], ['Write', 1], ['Close', 0],
255
- ['Seek', 2], ['ReadAt', 2], ['WriteAt', 2], ['ReadByte', 0],
256
- ['WriteByte', 1], ['ReadRune', 0], ['UnreadByte', 0], ['UnreadRune', 0],
257
- ['MarshalJSON', 0], ['UnmarshalJSON', 1], ['MarshalText', 0],
258
- ['UnmarshalText', 1], ['MarshalBinary', 0], ['UnmarshalBinary', 1],
259
- ['Scan', 1], ['Value', 0], ['LogValue', 0], ['Unwrap', 0],
260
- ['Is', 1], ['As', 1],
261
- ]);
285
+ /**
286
+ * A member a subtype or a structural satisfier MUST supply. A parser fact
287
+ * (`requiredMember`), read from the declaration's own syntax: an interface
288
+ * or object-type signature that is not optional, an abstract member, a
289
+ * trait method without a body, a Go interface method element, a Python
290
+ * abstract or Protocol member, a C++ pure virtual. A default-bodied
291
+ * interface or trait member is an implementation, not a requirement.
292
+ */
293
+ function isRequiredDeclaration(member) {
294
+ return member.requiredMember === true;
295
+ }
262
296
 
263
- function goImplicitExternalContract(symbol) {
264
- if (!symbol.className || !GO_PROTOCOL_METHODS.has(symbol.name)) return false;
297
+ /**
298
+ * Is this member held in place by a contract of the PROJECT? Invisible to a
299
+ * name count, because a same-name declaration line is a declaration, never
300
+ * a use. Returns the reason, or null:
301
+ *
302
+ * 'required-slot' it is the implementation a type owes to an
303
+ * interface, abstract class or trait it declares
304
+ * (deleting it leaves the type incomplete); or,
305
+ * where interfaces are satisfied implicitly by
306
+ * method set, the method that makes a type satisfy
307
+ * a project interface; or a descendant declares
308
+ * that it overrides it (deleting it leaves an
309
+ * override of nothing);
310
+ * 'structural-contract' its type has every member a project interface,
311
+ * object type or protocol requires, so the type
312
+ * can be used as that contract without naming it.
313
+ *
314
+ * A plain override of a concrete method is neither: with it gone, calls
315
+ * fall back to the inherited one.
316
+ */
317
+ function requiredByProjectContract(index, symbol) {
318
+ if (!symbol.className) {
319
+ // A member of an object literal declared against a project
320
+ // interface (`const impl: Contract = { slot() {} }`): the literal
321
+ // owes the interface that member unless the interface says it is
322
+ // optional. An out-of-project supertype may require it too.
323
+ const declaredAs = typedLiteralContract(index, symbol)?.projectInterface;
324
+ if (!declaredAs) return null;
325
+ const contracts = (index.symbols.get(declaredAs.name) || []).filter(type =>
326
+ _CLASS_KINDS.includes(type.type) && (!declaredAs.file || type.file === declaredAs.file));
327
+ const requirement = requirementIn(index, contracts, symbol.name);
328
+ return requirement === 'required' || requirement === 'unknown' ? 'required-slot' : null;
329
+ }
330
+ // An implementation written against this contract BY NAME holds the
331
+ // member it implements, required or default-bodied: Rust's
332
+ // `impl Contract for T { fn slot() }` and C#'s `void IContract.Slot()`
333
+ // are errors once the contract has no such member. Matched on the
334
+ // contract's bare name: wider than identity, and only ever withholds.
335
+ const contractNamedBy = member => {
336
+ const named = member.traitName || member.explicitInterface;
337
+ return named ? String(named).replace(/<.*$/s, '').split('::').pop().split('.').pop().trim() : null;
338
+ };
339
+ if ((index.symbols.get(symbol.name) || []).some(member =>
340
+ member !== symbol && contractNamedBy(member) === symbol.className)) return 'required-slot';
341
+ // A requirement is the contract, not an implementation of one: only a
342
+ // descendant that says it overrides it (below) holds it in place.
343
+ const isRequirement = isRequiredDeclaration(symbol) ||
344
+ (index.symbols.get(symbol.className) || []).some(type =>
345
+ (type.type === 'interface' || type.type === 'trait') &&
346
+ path.dirname(type.file) === path.dirname(symbol.file));
347
+ if (!isRequirement) {
348
+ // Interfaces satisfied implicitly by method set: the closure joins
349
+ // a project interface when the type satisfies it or is converted
350
+ // to it.
351
+ const implicit = goContractClosure(index, symbol);
352
+ const joined = implicit && implicit.interfaceNames;
353
+ if (joined && (joined.size ?? joined.length ?? 0) > 0) return 'required-slot';
354
+ }
355
+ const closure = hierarchySlotClosure(index, symbol);
356
+ if (closure) {
357
+ if (!isRequirement && (closure.ancestors || []).some(ancestor =>
358
+ (ancestor.members || []).some(member => isRequiredDeclaration(member)))) return 'required-slot';
359
+ if ((closure.descendants || []).some(descendant =>
360
+ (descendant.members || []).some(member => isOverrideMarked(member)))) return 'required-slot';
361
+ }
362
+ if (!isRequirement && structuralContractsRequiring(index, symbol).length > 0) return 'structural-contract';
363
+ return null;
364
+ }
365
+
366
+ /**
367
+ * What holds a declaration in place whatever names it: facts about the
368
+ * declaration itself, the same for an audit, a deletion and an addition.
369
+ * runtimeContract the language runtime or the compiler invokes it;
370
+ * externalContract it implements or overrides something defined outside
371
+ * the project;
372
+ * projectContract a contract of the project requires it (the reason:
373
+ * 'required-slot' or 'structural-contract'), or null;
374
+ * registered a decorator or a framework registration may invoke
375
+ * it without naming it.
376
+ */
377
+ function contractFactsOf(index, declared, lang) {
378
+ return {
379
+ runtimeContract: !!(protocolMemberOf(index, declared) || protocolTypeOf(index, declared)),
380
+ externalContract: !!(CLASS_AUDIT_KINDS.includes(declared.type)
381
+ ? _heritageReachesExternalBase(index, declared, lang, false)
382
+ : overridesOutOfTreeBase(index, declared) ||
383
+ typedLiteralExternalMember(index, declared) ||
384
+ implicitStandardContract(declared, lang)),
385
+ projectContract: requiredByProjectContract(index, declared),
386
+ registered: !!(isFrameworkEntrypoint(declared, index) || hasUnknownRegistrationDecorator(declared, index)),
387
+ };
388
+ }
389
+
390
+ /**
391
+ * An absent declaration, placed back in its owner for the questions that
392
+ * depend on where a member stands (which type owns it, what that type
393
+ * implements, what overrides it). Its recorded lines describe a revision
394
+ * the index does not hold; the owner's lines are the index's. Returns the
395
+ * record itself when it has no owner type, a re-anchored copy when its
396
+ * owner is the one type of that name in its file, the record unchanged when
397
+ * the owner is declared elsewhere (a receiver method, an impl block, an
398
+ * out-of-class definition), and null when several types of that name share
399
+ * the file and nothing says which one it stood in.
400
+ */
401
+ function anchoredInOwner(index, symbol) {
402
+ if (!symbol.className) return symbol;
403
+ const owners = (index.symbols.get(symbol.className) || []).filter(type =>
404
+ _CLASS_KINDS.includes(type.type) && type.file === symbol.file);
405
+ if (owners.length > 1) return null;
406
+ if (owners.length === 0) return { ...symbol };
407
+ const line = owners[0].endLine || owners[0].startLine;
408
+ const anchored = { ...symbol, startLine: line, endLine: line };
409
+ delete anchored.nameLine;
410
+ delete anchored.sourceSpan;
411
+ return anchored;
412
+ }
413
+
414
+ /**
415
+ * Run `question` with an absent declaration declared again: the record
416
+ * joins the name's definitions for the duration, and every index derived
417
+ * from them (contract closures, class identity) is rebuilt before and
418
+ * dropped after, so nothing computed with it outlives the call.
419
+ */
420
+ function withDeclared(index, declared, question) {
421
+ const { resetClassIdentity } = require('./class-identity');
422
+ const existing = index.symbols.get(declared.name);
423
+ if (existing) existing.push(declared);
424
+ else index.symbols.set(declared.name, [declared]);
425
+ resetContractIndex(index);
426
+ resetClassIdentity(index);
427
+ try {
428
+ return question(declared);
429
+ } finally {
430
+ if (existing) existing.splice(existing.lastIndexOf(declared), 1);
431
+ else index.symbols.delete(declared.name);
432
+ resetContractIndex(index);
433
+ resetClassIdentity(index);
434
+ }
435
+ }
436
+
437
+ /**
438
+ * A method shaped like a member of a standard contract the language lets a
439
+ * type satisfy without naming it (trait `implicitStandardContracts`: Go's
440
+ * sort.Interface, http.Handler, io.Reader, the encoding marshalers ...).
441
+ */
442
+ function implicitStandardContract(symbol, lang) {
443
+ const methods = langTraits(lang)?.implicitStandardContracts;
444
+ if (!methods || !symbol.className || !methods.has(symbol.name)) return false;
265
445
  const arity = Array.isArray(symbol.paramsStructured)
266
446
  ? symbol.paramsStructured.length
267
447
  : null;
268
- return arity == null || arity === GO_PROTOCOL_METHODS.get(symbol.name);
448
+ return arity == null || arity === methods.get(symbol.name);
269
449
  }
270
450
 
271
451
  // Symbol types whose definition NAME line provably cannot reference a
@@ -364,53 +544,37 @@ function nameOnlySelfRecursive(index, name) {
364
544
  * part of both text and JSON output.
365
545
  */
366
546
  function scanDeadcodeCoverage(index, names) {
367
- const matchedNames = new Set();
368
- const files = new Set();
369
- const unreadableFiles = new Set();
370
547
  const reasons = {};
371
- let unknownCoverage = false;
372
548
  let parseRecoveries = 0;
373
549
 
374
- const recordReason = (reason) => {
375
- const key = reason || 'unknown';
376
- reasons[key] = (reasons[key] || 0) + 1;
377
- };
378
- const scanContent = (content) => {
379
- const identifiers = new Set(String(content).match(/\b[a-zA-Z_]\w*\b/g) || []);
380
- for (const name of names) {
381
- if (/^[a-zA-Z_]\w*$/.test(name)
382
- ? identifiers.has(name)
383
- : String(content).includes(name)) matchedNames.add(name);
384
- }
385
- };
386
- const scanFile = (absPath, rel, reason) => {
387
- files.add(rel);
388
- recordReason(reason);
389
- try {
390
- const stat = fs.statSync(absPath);
391
- if (!stat.isFile()) {
392
- unknownCoverage = true;
393
- return;
394
- }
395
- scanContent(index._readFile(absPath));
396
- } catch (_) {
397
- unreadableFiles.add(rel);
398
- unknownCoverage = true;
399
- }
400
- };
401
-
550
+ // One classification and one scanner for every command
551
+ // (core/source-universe.js): parse failures and text-only source are
552
+ // searched for the candidate names in a single pass per file; a path
553
+ // nothing can be read from withdraws every claim.
554
+ const sourceUniverse = require('./source-universe');
555
+ const failed = [];
402
556
  for (const failedPath of index.failedFiles || []) {
403
557
  if (index.files.has(failedPath)) continue;
404
- scanFile(failedPath, path.relative(index.root, failedPath), 'parse-failure');
405
- }
406
- for (const skipped of index.unsupportedFiles || []) {
407
- const rel = skipped.relativePath;
408
- scanFile(path.join(index.root, rel), rel, 'unsupported-language');
558
+ failed.push({
559
+ absolutePath: failedPath,
560
+ relativePath: path.relative(index.root, failedPath),
561
+ reason: 'parse-failure',
562
+ });
409
563
  }
410
- for (const issue of index.discoveryIssues || []) {
411
- const rel = issue.relativePath || '.';
412
- scanFile(path.join(index.root, rel), rel, issue.reason || 'discovery-gap');
564
+ const scanned = sourceUniverse.scanTextOnlyNames(index, names, failed);
565
+ const matchedNames = scanned.matchedNames;
566
+ const files = new Set(scanned.files);
567
+ const unreadableFiles = new Set(scanned.unreadableFiles);
568
+ for (const [reason, count] of Object.entries(scanned.reasons)) reasons[reason] = count;
569
+ const { opaque } = sourceUniverse.universe(index);
570
+ for (const entry of opaque) {
571
+ files.add(entry.relativePath);
572
+ const key = entry.reason || 'discovery-gap';
573
+ reasons[key] = (reasons[key] || 0) + 1;
574
+ if (entry.reason === 'unreadable-file') unreadableFiles.add(entry.relativePath);
413
575
  }
576
+ const unknownCoverage = opaque.length > 0 || unreadableFiles.size > 0;
577
+
414
578
  for (const [, fileEntry] of index.files) {
415
579
  if (fileEntry.parseRecovery || fileEntry.parseError) parseRecoveries++;
416
580
  }
@@ -748,12 +912,79 @@ function deadcode(index, options = {}) {
748
912
  index._beginOp();
749
913
  try {
750
914
  const results = [];
915
+ // This pipeline IS the deletion eligibility rule. The audit runs it over
916
+ // every name; a deletion assessment (core/deletion.js) runs the same
917
+ // code over one name and asks why each declaration was or was not
918
+ // claimed. `names` restricts the pass, `explain(symbol, reason, detail)`
919
+ // reports every outcome (detail: the audit row of a claim, the sites of
920
+ // a use). Neither changes what is claimed.
921
+ const onlyNames = options.names instanceof Set ? options.names : null;
922
+ const packagingEntrypoints = runtimeEntrypoints(index);
923
+ const explain = typeof options.explain === 'function' ? options.explain : null;
924
+ const withhold = (symbol, reason, detail) => { if (explain) explain(symbol, reason, detail); };
925
+ // Assessment only: names whose every call site the caller has shown to
926
+ // lie inside the declaration being assessed. Those calls are deleted
927
+ // with it, so the name goes on to the text scan under the recursion
928
+ // carve-out below. The audit never sets this.
929
+ const ownBodyOnly = options.ownBodyOnly instanceof Set ? options.ownBodyOnly : null;
930
+ // Declarations the index does not hold (deleted since, or read from a
931
+ // base revision), assessed from their own record against this index's
932
+ // uses of the name. Their line numbers describe a file that no longer
933
+ // has them, so they never mark a definition site in the current text.
934
+ const absent = new Map(); // symbol record -> file entry of its revision
935
+ const absentByName = new Map();
936
+ for (const extra of Array.isArray(options.declarations) ? options.declarations : []) {
937
+ const symbol = extra.symbol;
938
+ if (!symbol || !symbol.name) continue;
939
+ if (!symbol.relativePath) {
940
+ symbol.relativePath = path.relative(index.root, symbol.file).split(path.sep).join('/');
941
+ }
942
+ // With no file facts from the revision it was declared in, only what
943
+ // the record itself says is known: nothing is exported by a list the
944
+ // file no longer has.
945
+ absent.set(symbol, extra.fileEntry || index.files.get(symbol.file) || {
946
+ language: detectLanguage(symbol.file, index.root),
947
+ relativePath: symbol.relativePath,
948
+ symbols: [], exports: [], exportDetails: [], imports: [], bindings: [],
949
+ });
950
+ if (!absentByName.has(symbol.name)) absentByName.set(symbol.name, []);
951
+ absentByName.get(symbol.name).push(symbol);
952
+ }
953
+ // Where a member stands decides what it implements and what overrides
954
+ // it. An absent declaration answers those questions standing in its
955
+ // owner again (anchoredInOwner); null means it cannot be placed, and
956
+ // its contracts cannot be checked.
957
+ const anchored = new Map();
958
+ for (const symbol of absent.keys()) anchored.set(symbol, anchoredInOwner(index, symbol));
959
+ // The answers that depend on where a member stands. For an absent
960
+ // declaration they are computed together, under one overlay.
961
+ const standingFacts = (declared, lang) => contractFactsOf(index, declared, lang);
962
+ const absentFacts = new Map();
963
+ const standing = (symbol, lang) => {
964
+ if (!absent.has(symbol)) return null;
965
+ if (!absentFacts.has(symbol)) {
966
+ absentFacts.set(symbol, withDeclared(index, anchored.get(symbol),
967
+ declared => standingFacts(declared, lang)));
968
+ }
969
+ return absentFacts.get(symbol);
970
+ };
971
+ const symbolsOf = name => (absentByName.has(name)
972
+ ? [...(index.symbols.get(name) || []), ...absentByName.get(name)]
973
+ : index.symbols.get(name) || []);
974
+ const fileEntryOf = symbol => absent.get(symbol) || index.files.get(symbol.file);
975
+ const namedSymbols = absent.size === 0 ? index.symbols : (function* merged() {
976
+ for (const [name] of index.symbols) yield [name, symbolsOf(name)];
977
+ for (const name of absentByName.keys()) {
978
+ if (!index.symbols.has(name)) yield [name, symbolsOf(name)];
979
+ }
980
+ })();
751
981
  let excludedDecorated = 0;
752
982
  let excludedExported = 0;
753
983
  let excludedExternalContract = 0;
754
984
  let excludedRuntimeContract = 0;
755
985
  let excludedConstructors = 0;
756
986
  let excludedDynamicDispatch = 0;
987
+ let excludedRequiredSlot = 0;
757
988
  const computedDispatchByFile = projectComputedDispatch(index);
758
989
  const computedDispatchReceivers = new Set();
759
990
  const computedDispatchInfo = { count: 0, fileCount: 0, files: [] };
@@ -794,8 +1025,8 @@ function deadcode(index, options = {}) {
794
1025
  if (!fe || sites.length === 0) continue;
795
1026
  for (const site of sites) {
796
1027
  if (site.dynamic) dynamicReflectionFiles.add(reflectionFile);
797
- if (site.name) literalReflectionNames.add(site.name);
798
- if (site.patterns && site.patterns.length > 0 && !site.name) {
1028
+ if (site.name && !site.runtimeTarget) literalReflectionNames.add(site.name);
1029
+ if (site.patterns && site.patterns.length > 0 && !site.name && !site.runtimeTarget) {
799
1030
  // Exact alternatives of a multi-spelling site are literals.
800
1031
  for (const pattern of site.patterns) {
801
1032
  if (!pattern.includes('*')) literalReflectionNames.add(pattern);
@@ -843,10 +1074,15 @@ function deadcode(index, options = {}) {
843
1074
  const classAuditSet = new Set(CLASS_AUDIT_KINDS);
844
1075
  const callableNames = new Set();
845
1076
  for (const [symbolName, symbols] of index.symbols) {
1077
+ if (onlyNames && !onlyNames.has(symbolName)) continue;
846
1078
  if (symbols.some(s => auditTypeSet.has(s.type))) {
847
1079
  callableNames.add(symbolName);
848
1080
  }
849
1081
  }
1082
+ for (const [symbolName, symbols] of absentByName) {
1083
+ if (onlyNames && !onlyNames.has(symbolName)) continue;
1084
+ if (symbols.some(s => auditTypeSet.has(s.type))) callableNames.add(symbolName);
1085
+ }
850
1086
 
851
1087
  // Pre-filter: names in the callee index have call sites → definitely used → not dead.
852
1088
  // Exception (fix #253c): a name whose EVERY call site sits inside a
@@ -858,7 +1094,7 @@ function deadcode(index, options = {}) {
858
1094
  for (const name of callableNames) {
859
1095
  if (!index.calleeIndex.has(name)) {
860
1096
  potentiallyDeadNames.add(name);
861
- } else if (nameOnlySelfRecursive(index, name)) {
1097
+ } else if ((ownBodyOnly && ownBodyOnly.has(name)) || nameOnlySelfRecursive(index, name)) {
862
1098
  selfRecursiveNames.add(name);
863
1099
  potentiallyDeadNames.add(name);
864
1100
  }
@@ -867,12 +1103,12 @@ function deadcode(index, options = {}) {
867
1103
  // Pre-filter exported symbols from the scan set when not auditing exports.
868
1104
  // Go exports ~63K capitalized names on K8s — scanning these in Phase 2 only to
869
1105
  // skip them in Phase 3 wastes O(63K × 11K files) = ~700M comparisons.
870
- if (!options.includeExported) {
1106
+ if (downstreamExposureBlocks(index, true, options)) {
871
1107
  const narrowed = new Set();
872
1108
  for (const name of potentiallyDeadNames) {
873
- const syms = index.symbols.get(name) || [];
1109
+ const syms = symbolsOf(name);
874
1110
  // Keep the name only if at least one definition is NOT exported
875
- const allExported = syms.every(s => symbolIsExported(index, s, index.files.get(s.file)));
1111
+ const allExported = syms.every(s => symbolIsExported(index, s, fileEntryOf(s)));
876
1112
  if (!allExported) narrowed.add(name);
877
1113
  }
878
1114
  potentiallyDeadNames = narrowed;
@@ -884,7 +1120,7 @@ function deadcode(index, options = {}) {
884
1120
  if (options.file) {
885
1121
  const filteredNames = new Set();
886
1122
  for (const name of potentiallyDeadNames) {
887
- const syms = index.symbols.get(name) || [];
1123
+ const syms = symbolsOf(name);
888
1124
  if (syms.some(s => s.relativePath && s.relativePath.includes(options.file))) {
889
1125
  filteredNames.add(name);
890
1126
  }
@@ -937,11 +1173,6 @@ function deadcode(index, options = {}) {
937
1173
  }
938
1174
 
939
1175
  const coverage = scanDeadcodeCoverage(index, potentiallyDeadNames);
940
- const coverageSuppressedNames = new Set([
941
- ...coverage.matchedNames,
942
- ...literalReflectionNames,
943
- ...macroSuppressedNames,
944
- ]);
945
1176
  if (coverage.claimsWithdrawn) {
946
1177
  // An unreadable file/directory can contain a use of any candidate.
947
1178
  // Returning no deletion candidates is the only sound verdict.
@@ -1087,6 +1318,18 @@ function deadcode(index, options = {}) {
1087
1318
  // name inside /* ... */ counted as usage — commented-out code
1088
1319
  // silently kept its symbols "alive" and hid true dead claims.
1089
1320
  const lines = maskBlockComments(content, fileEntry.language).split('\n');
1321
+ let stringTree;
1322
+ const multilinePython = fileEntry.language === 'python' && (content.includes('"""') || content.includes("'''"));
1323
+ const inMultilineString = (line, column) => {
1324
+ if (!multilinePython) return false;
1325
+ stringTree ||= index._getParsedTree(filePath, content, fileEntry.language);
1326
+ let node = stringTree.rootNode.descendantForPosition({ row: line, column });
1327
+ for (; node; node = node.parent) {
1328
+ if (node.type === 'interpolation') return false;
1329
+ if (node.type === 'string') return true;
1330
+ }
1331
+ return false;
1332
+ };
1090
1333
  for (const name of namesInFile) {
1091
1334
  const nameLen = name.length;
1092
1335
  for (let i = 0; i < lines.length; i++) {
@@ -1122,7 +1365,7 @@ function deadcode(index, options = {}) {
1122
1365
  const rustAttributePath = fileEntry.language === 'rust' &&
1123
1366
  /^\s*#\[\s*serde\s*\(/.test(line) &&
1124
1367
  new RegExp(`\\b(?:serialize_with|deserialize_with|default|with)\\s*=\\s*["'][^"']*\\b${escapeRegExp(name)}\\b`).test(line);
1125
- if (isInsideString(line, pos, fileEntry.language) &&
1368
+ if ((isInsideString(line, pos, fileEntry.language) || inMultilineString(i, pos)) &&
1126
1369
  !(fileEntry.language === 'python' && classKindNames.has(name)) &&
1127
1370
  !rustAttributePath) continue;
1128
1371
  // Property/field access (preceded by '.'), not a
@@ -1245,8 +1488,16 @@ function deadcode(index, options = {}) {
1245
1488
  }
1246
1489
  }
1247
1490
 
1248
- for (const [name, symbols] of index.symbols) {
1249
- if (coverage.claimsWithdrawn || coverageSuppressedNames.has(name)) continue;
1491
+ for (const [name, symbols] of namedSymbols) {
1492
+ if (onlyNames && !onlyNames.has(name)) continue;
1493
+ // A name-level withdrawal: no declaration of this name is claimed.
1494
+ // The audit skips the name; an assessment still walks the chain so
1495
+ // a more specific reason (the name has callers) is reported first.
1496
+ const withdrawn = coverage.claimsWithdrawn ? 'unsearchable-source'
1497
+ : literalReflectionNames.has(name) ? 'reflection'
1498
+ : macroSuppressedNames.has(name) ? 'macro-reference'
1499
+ : coverage.matchedNames.has(name) ? 'unanalyzed-occurrence' : null;
1500
+ if (withdrawn && !explain) continue;
1250
1501
  // Definition NAME lines of same-name def-kind symbols are
1251
1502
  // declarations, not usages — two never-called same-name methods used
1252
1503
  // to keep each other alive (fix #243: three unreferenced `delete`
@@ -1257,6 +1508,7 @@ function deadcode(index, options = {}) {
1257
1508
  if (sameNameDefLines) return sameNameDefLines;
1258
1509
  sameNameDefLines = new Map();
1259
1510
  for (const other of symbols) {
1511
+ if (absent.has(other)) continue;
1260
1512
  if (!DEF_NAME_LINE_KINDS.has(other.type)) continue;
1261
1513
  const ln = other.nameLine ?? other.startLine;
1262
1514
  if (ln == null) continue;
@@ -1277,6 +1529,7 @@ function deadcode(index, options = {}) {
1277
1529
  if (sameNameDefRanges) return sameNameDefRanges;
1278
1530
  sameNameDefRanges = new Map();
1279
1531
  for (const other of symbols) {
1532
+ if (absent.has(other)) continue;
1280
1533
  if (!DEF_NAME_LINE_KINDS.has(other.type)) continue;
1281
1534
  let arr = sameNameDefRanges.get(other.file);
1282
1535
  if (!arr) { arr = []; sameNameDefRanges.set(other.file, arr); }
@@ -1288,6 +1541,7 @@ function deadcode(index, options = {}) {
1288
1541
  for (const symbol of symbols) {
1289
1542
  // Skip non-audited types (callableTypes defined above)
1290
1543
  if (!auditTypeSet.has(symbol.type)) {
1544
+ withhold(symbol, 'kind-not-audited');
1291
1545
  continue;
1292
1546
  }
1293
1547
 
@@ -1297,19 +1551,22 @@ function deadcode(index, options = {}) {
1297
1551
  // surface (ECMA-262 body-only scope). Auditing them claims
1298
1552
  // callback handlers dead (`app.on('ready', function boot(){})`).
1299
1553
  if (symbol.bodyScopedName) {
1554
+ withhold(symbol, 'expression-scoped-name');
1300
1555
  continue;
1301
1556
  }
1302
1557
  // Generated by a project macro_rules! invocation (fix #374): the
1303
1558
  // definition is the macro's template or the invocation's
1304
1559
  // arguments, never a deletable declaration of its own.
1305
1560
  if (symbol.macroExpansion) {
1561
+ withhold(symbol, 'macro-generated');
1306
1562
  continue;
1307
1563
  }
1308
- const fileEntry = index.files.get(symbol.file);
1564
+ const fileEntry = fileEntryOf(symbol);
1309
1565
  const lang = fileEntry?.language;
1310
1566
 
1311
1567
  // Skip bundled/minified files (webpack bundles, build artifacts)
1312
1568
  if (fileEntry?.isBundled) {
1569
+ withhold(symbol, 'bundled-file');
1313
1570
  continue;
1314
1571
  }
1315
1572
 
@@ -1321,6 +1578,7 @@ function deadcode(index, options = {}) {
1321
1578
  if (/^operator(?:\b|[^a-zA-Z0-9_$])/.test(symbol.name) ||
1322
1579
  symbol.name === 'this[]' ||
1323
1580
  ((lang === 'c' || lang === 'cpp') && symbol.name.startsWith('~'))) {
1581
+ withhold(symbol, 'operator-surface');
1324
1582
  continue;
1325
1583
  }
1326
1584
 
@@ -1330,22 +1588,26 @@ function deadcode(index, options = {}) {
1330
1588
  if (lang === 'rust' && (symbol.modifiers || []).some(modifier =>
1331
1589
  modifier === 'pymethods' || modifier === 'pymodule_init')) {
1332
1590
  excludedRuntimeContract++;
1591
+ withhold(symbol, 'runtime-contract');
1333
1592
  continue;
1334
1593
  }
1335
1594
 
1336
1595
  // Skip test files unless requested
1337
1596
  if (!options.includeTests && isTestFile(symbol.relativePath, lang)) {
1597
+ withhold(symbol, 'test-file');
1338
1598
  continue;
1339
1599
  }
1340
1600
 
1341
1601
  // Apply file filter (scopes deadcode to matching files)
1342
1602
  if (options.file && !symbol.relativePath.includes(options.file)) {
1603
+ withhold(symbol, 'out-of-scope');
1343
1604
  continue;
1344
1605
  }
1345
1606
 
1346
1607
  // Apply exclude and in filters
1347
1608
  if ((options.exclude && options.exclude.length > 0) || options.in) {
1348
1609
  if (!index.matchesFilters(symbol.relativePath, { exclude: options.exclude, in: options.in })) {
1610
+ withhold(symbol, 'out-of-scope');
1349
1611
  continue;
1350
1612
  }
1351
1613
  }
@@ -1358,6 +1620,7 @@ function deadcode(index, options = {}) {
1358
1620
  // compile time and describes external/global shapes — nothing in
1359
1621
  // it is deletable code, so nothing in it is claimable.
1360
1622
  if (symbol.relativePath.endsWith('.d.ts')) {
1623
+ withhold(symbol, 'ambient-declaration');
1361
1624
  continue;
1362
1625
  }
1363
1626
  // The same holds for a `declare global` / `declare module 'x'`
@@ -1365,6 +1628,7 @@ function deadcode(index, options = {}) {
1365
1628
  // with declarations outside the project, whose readers UCN
1366
1629
  // cannot see (`interface Window`, a library's `Register`).
1367
1630
  if (symbol.ambientAugmentation) {
1631
+ withhold(symbol, 'ambient-declaration');
1368
1632
  continue;
1369
1633
  }
1370
1634
 
@@ -1372,6 +1636,7 @@ function deadcode(index, options = {}) {
1372
1636
  // Each language module declares its own isEntryPoint() rules.
1373
1637
  const langModule = getLanguageAdapter(lang);
1374
1638
  if (langModule.isEntryPoint?.(symbol)) {
1639
+ withhold(symbol, 'entry-point');
1375
1640
  continue;
1376
1641
  }
1377
1642
  // A constructor participates in the type's allocation protocol,
@@ -1385,14 +1650,24 @@ function deadcode(index, options = {}) {
1385
1650
  (symbol.className && !classAuditSet.has(symbol.type) &&
1386
1651
  langTraits(lang)?.constructorProtocol?.initializer === name)) {
1387
1652
  excludedConstructors++;
1653
+ withhold(symbol, 'constructor');
1388
1654
  continue;
1389
1655
  }
1390
1656
  // Members the runtime/compiler invokes by name (trait
1391
1657
  // languageProtocolMember): Java serialization callbacks on
1392
1658
  // Serializable types, Python Enum hooks on Enum subclasses, C#
1393
1659
  // pattern-bound members, JS toJSON/then/[Symbol.*] (fix #363).
1394
- if (protocolMemberOf(index, symbol) || protocolTypeOf(index, symbol)) {
1660
+ // An absent member that cannot be placed in one owner type
1661
+ // cannot have its contracts checked: no claim, and no guess.
1662
+ if (absent.has(symbol) && anchored.get(symbol) === null) {
1663
+ withhold(symbol, 'owner-ambiguous');
1664
+ continue;
1665
+ }
1666
+ if (standing(symbol, lang)
1667
+ ? standing(symbol, lang).runtimeContract
1668
+ : protocolMemberOf(index, symbol) || protocolTypeOf(index, symbol)) {
1395
1669
  excludedRuntimeContract++;
1670
+ withhold(symbol, 'runtime-contract');
1396
1671
  continue;
1397
1672
  }
1398
1673
 
@@ -1402,6 +1677,7 @@ function deadcode(index, options = {}) {
1402
1677
  // contract (CS0535); classify it with other external contracts.
1403
1678
  if (lang === 'csharp' && symbol.explicitInterface) {
1404
1679
  excludedExternalContract++;
1680
+ withhold(symbol, 'external-contract');
1405
1681
  continue;
1406
1682
  }
1407
1683
 
@@ -1410,7 +1686,18 @@ function deadcode(index, options = {}) {
1410
1686
  // call-pattern-based registration (Express routes, Gin handlers, etc.)
1411
1687
  // These functions are invoked by frameworks, not by user code.
1412
1688
  const hasFrameworkEntrypoint = isFrameworkEntrypoint(symbol, index);
1413
- const hasDecoratorMetadata = hasUnknownRegistrationDecorator(symbol);
1689
+ const hasDecoratorMetadata = hasUnknownRegistrationDecorator(symbol, index);
1690
+ if (lang === 'python' && packagingEntrypoints.some(entry => entry.unknown || runtimeTargetReaches(index, symbol, entry.target))) {
1691
+ withhold(symbol, 'runtime-entrypoint');
1692
+ continue;
1693
+ }
1694
+
1695
+ if (langTraits(lang)?.reflectionApi?.pythonBindingIdentity && symbol.registrationUnknown === false &&
1696
+ (symbol.decorators || []).length && pythonProjectIdentityAffected(index, 'builtins.*')) {
1697
+ excludedDecorated++;
1698
+ withhold(symbol, 'builtin-namespace-effect');
1699
+ continue;
1700
+ }
1414
1701
 
1415
1702
  // Unknown decorators are conservative runtime-registration
1416
1703
  // boundaries. A decorator necessarily consumes/replaces the
@@ -1420,6 +1707,20 @@ function deadcode(index, options = {}) {
1420
1707
  if ((hasFrameworkEntrypoint || hasDecoratorMetadata) &&
1421
1708
  !options.includeDecorated) {
1422
1709
  excludedDecorated++;
1710
+ withhold(symbol, 'registered');
1711
+ continue;
1712
+ }
1713
+
1714
+ // The caller graph retains source spellings. Python can bind a
1715
+ // different written identifier to this runtime name via NFKC;
1716
+ // until that graph models the equivalence, do not clear it. The
1717
+ // inventory is AST-only and shared by every symbol in this op.
1718
+ if (langTraits(lang)?.reflectionApi?.pythonBindingIdentity &&
1719
+ (pythonSourceName(symbol.name) !== symbol.name ||
1720
+ (symbol.runtimeOwnerPath || [symbol.className]).filter(Boolean)
1721
+ .some(owner => pythonSourceName(owner) !== owner) ||
1722
+ pythonNormalizedNames(index).has(pythonSourceName(symbol.name)))) {
1723
+ withhold(symbol, 'identifier-normalization');
1423
1724
  continue;
1424
1725
  }
1425
1726
 
@@ -1433,18 +1734,25 @@ function deadcode(index, options = {}) {
1433
1734
  // explicitly enables exported-surface review.
1434
1735
  if (symbol.memberAssigned && !symbol.registryMember && !isExported) {
1435
1736
  excludedRuntimeContract++;
1737
+ withhold(symbol, 'runtime-contract');
1436
1738
  continue;
1437
1739
  }
1438
1740
 
1439
1741
  // Skip exported unless requested
1440
- if (isExported && !options.includeExported) {
1742
+ // POLICY, not fact: an exported symbol may have consumers outside
1743
+ // the repository. A library keeps it out of the audit; a project
1744
+ // declared an application, or --include-exported, audits it.
1745
+ // `isExported` stays the fact other shields read (above).
1746
+ if (downstreamExposureBlocks(index, isExported, options)) {
1441
1747
  excludedExported++;
1748
+ withhold(symbol, 'downstream-exposure');
1442
1749
  continue;
1443
1750
  }
1444
1751
 
1445
1752
  // Fast path: name has call sites in callee index → definitely used → not dead
1446
1753
  // (unless every site is the name's own recursion — fix #253c).
1447
1754
  if (index.calleeIndex.has(name) && !selfRecursiveNames.has(name)) {
1755
+ withhold(symbol, 'has-callers');
1448
1756
  continue;
1449
1757
  }
1450
1758
  // Class-kind claims (fix #253a): a class whose member the runtime
@@ -1460,15 +1768,20 @@ function deadcode(index, options = {}) {
1460
1768
  // live. This is the normal invocation form for C# extension
1461
1769
  // containers and other static helper types.
1462
1770
  if (members.some(m => index.calleeIndex.has(m.name))) {
1771
+ withhold(symbol, 'member-in-use', explain && {
1772
+ members: members.filter(m => index.calleeIndex.has(m.name)).map(m => m.name),
1773
+ });
1463
1774
  continue;
1464
1775
  }
1465
1776
  if (members.some(m => langModule.isEntryPoint?.(m))) {
1777
+ withhold(symbol, 'entry-point');
1466
1778
  continue;
1467
1779
  }
1468
1780
  if (members.some(m => isFrameworkEntrypoint(m, index) ||
1469
- hasUnknownRegistrationDecorator(m))) {
1781
+ hasUnknownRegistrationDecorator(m, index))) {
1470
1782
  if (!options.includeDecorated) {
1471
1783
  excludedDecorated++;
1784
+ withhold(symbol, 'registered');
1472
1785
  continue;
1473
1786
  }
1474
1787
  }
@@ -1484,7 +1797,9 @@ function deadcode(index, options = {}) {
1484
1797
  // resolves to — `user.load` keeps user.js's load alive, never an
1485
1798
  // unrelated module's same-name symbol.
1486
1799
  let nonDefUsages = allUsages.filter(u =>
1487
- !(u.file === symbol.file && (u.line === symbol.startLine || u.line === symbol.nameLine)) &&
1800
+ // (an absent declaration has no definition site in this text)
1801
+ !(!absent.has(symbol) && u.file === symbol.file &&
1802
+ (u.line === symbol.startLine || u.line === symbol.nameLine)) &&
1488
1803
  !(defNameLines().get(u.file)?.has(u.line)) &&
1489
1804
  // Self-recursion carve-out names (fix #253c): a reference
1490
1805
  // inside a same-name def's own body is the recursion itself.
@@ -1510,7 +1825,7 @@ function deadcode(index, options = {}) {
1510
1825
  // AST ranges, not line prefixes (fix #247): a multi-line
1511
1826
  // `export { a,\n b }` block's continuation lines counted as
1512
1827
  // consumption, silently hiding every symbol exported that way.
1513
- if (isExported && options.includeExported) {
1828
+ if (isExported && !downstreamExposureBlocks(index, isExported, options)) {
1514
1829
  nonDefUsages = nonDefUsages.filter(u => {
1515
1830
  if (u.file !== symbol.file) return true; // cross-file usage always counts
1516
1831
  const ranges = exportSiteRanges(u.file);
@@ -1521,7 +1836,15 @@ function deadcode(index, options = {}) {
1521
1836
  // Total includes all usage types (calls, references, callbacks, re-exports)
1522
1837
  const totalUsages = nonDefUsages.length;
1523
1838
 
1839
+ if (totalUsages > 0) withhold(symbol, 'has-usages', explain && { usages: nonDefUsages });
1524
1840
  if (totalUsages === 0) {
1841
+ // A name-level withdrawal applies to every declaration the
1842
+ // chain did not already explain (assessment only: the audit
1843
+ // skipped this name before the chain).
1844
+ if (withdrawn) {
1845
+ withhold(symbol, withdrawn);
1846
+ continue;
1847
+ }
1525
1848
  // External-contract override: the method may be invoked through
1526
1849
  // an out-of-tree base class UCN can't index (deadcode analog of
1527
1850
  // fix #210). A zero usage count is not evidence of deadness.
@@ -1532,13 +1855,30 @@ function deadcode(index, options = {}) {
1532
1855
  // such subclasses non-textually (django `Command(BaseCommand)`
1533
1856
  // by module path, pytest plugins by registration) — zero
1534
1857
  // in-project references is not evidence of deadness there.
1535
- const isExternalContract = classAuditSet.has(symbol.type)
1536
- ? _heritageReachesExternalBase(index, symbol, lang, false)
1537
- : overridesOutOfTreeBase(index, symbol) ||
1538
- typedLiteralExternalMember(index, symbol) ||
1539
- (lang === 'go' && goImplicitExternalContract(symbol));
1858
+ const isExternalContract = standing(symbol, lang)
1859
+ ? standing(symbol, lang).externalContract
1860
+ : (classAuditSet.has(symbol.type)
1861
+ ? _heritageReachesExternalBase(index, symbol, lang, false)
1862
+ : overridesOutOfTreeBase(index, symbol) ||
1863
+ typedLiteralExternalMember(index, symbol) ||
1864
+ implicitStandardContract(symbol, lang));
1540
1865
  if (isExternalContract && !options.includeExported) {
1541
1866
  excludedExternalContract++;
1867
+ withhold(symbol, 'external-contract');
1868
+ continue;
1869
+ }
1870
+ // Held in place by a contract of the project itself: the
1871
+ // implementation a type owes to an interface, abstract class
1872
+ // or trait, a member a descendant declares it overrides, or
1873
+ // a member that lets its type stand in for a project
1874
+ // interface or protocol by shape. A fact about the
1875
+ // declaration, so no flag reveals it.
1876
+ const projectContract = standing(symbol, lang)
1877
+ ? standing(symbol, lang).projectContract
1878
+ : requiredByProjectContract(index, symbol);
1879
+ if (projectContract) {
1880
+ excludedRequiredSlot++;
1881
+ withhold(symbol, projectContract);
1542
1882
  continue;
1543
1883
  }
1544
1884
  // Collect decorators/annotations for hint display
@@ -1583,6 +1923,7 @@ function deadcode(index, options = {}) {
1583
1923
  computedDispatchReceivers.has(symbol.registryContainer);
1584
1924
  if (dynamicallyDispatched) {
1585
1925
  excludedDynamicDispatch++;
1926
+ withhold(symbol, 'dynamic-dispatch');
1586
1927
  continue;
1587
1928
  }
1588
1929
 
@@ -1599,6 +1940,7 @@ function deadcode(index, options = {}) {
1599
1940
  reflectionWithheldByPattern.set(pattern,
1600
1941
  (reflectionWithheldByPattern.get(pattern) || 0) + 1);
1601
1942
  reflectionInfo.withheld++;
1943
+ withhold(symbol, 'reflection');
1602
1944
  continue;
1603
1945
  }
1604
1946
 
@@ -1617,6 +1959,7 @@ function deadcode(index, options = {}) {
1617
1959
  reflectionInfo.withheldByTable.push({ name: symbol.name,
1618
1960
  file: symbol.relativePath, line, startLine: symbol.startLine,
1619
1961
  reason: 'literal-name-near-dynamic-reflection' });
1962
+ withhold(symbol, 'reflection');
1620
1963
  continue;
1621
1964
  }
1622
1965
  }
@@ -1640,6 +1983,7 @@ function deadcode(index, options = {}) {
1640
1983
  ...(declaredOn && { declaredOn }),
1641
1984
  ...(isExternalContract && { externalContract: true })
1642
1985
  });
1986
+ if (explain) explain(symbol, 'claimed', results[results.length - 1]);
1643
1987
  }
1644
1988
  }
1645
1989
  }
@@ -1657,6 +2001,7 @@ function deadcode(index, options = {}) {
1657
2001
  results.excludedRuntimeContract = excludedRuntimeContract;
1658
2002
  results.excludedConstructors = excludedConstructors;
1659
2003
  results.excludedDynamicDispatch = excludedDynamicDispatch;
2004
+ results.excludedRequiredSlot = excludedRequiredSlot;
1660
2005
  results.computedDispatch = computedDispatchInfo;
1661
2006
  reflectionInfo.withheldByTable.sort((a, b) => codeUnitCompare(a.file, b.file) ||
1662
2007
  a.startLine - b.startLine || codeUnitCompare(a.name, b.name));
@@ -1685,12 +2030,16 @@ function deadcode(index, options = {}) {
1685
2030
  reasons: coverage.reasons,
1686
2031
  parseRecoveries: coverage.parseRecoveries,
1687
2032
  };
1688
- results.pythonImplicitExportFiles = [...index.files.values()].filter(entry =>
1689
- isPythonPackageFile(index, entry) &&
1690
- !(entry.exportDetails || []).some(exp => exp.type === '__all__')).length;
2033
+ // The Python public-surface rule is an exposure rule: it shields nothing
2034
+ // in a project declared an application.
2035
+ results.projectKind = projectKind(index);
2036
+ results.pythonImplicitExportFiles = results.projectKind === 'application' ? 0
2037
+ : [...index.files.values()].filter(entry =>
2038
+ isPythonPackageFile(index, entry) &&
2039
+ !(entry.exportDetails || []).some(exp => exp.type === '__all__')).length;
1691
2040
 
1692
2041
  return results;
1693
2042
  } finally { index._endOp(); }
1694
2043
  }
1695
2044
 
1696
- module.exports = { buildUsageIndex, deadcode, nameOnlySelfRecursive, DEF_NAME_LINE_KINDS };
2045
+ module.exports = { buildUsageIndex, deadcode, nameOnlySelfRecursive, contractFactsOf, DEF_NAME_LINE_KINDS };