ucn 5.6.0 → 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.
@@ -19,7 +19,7 @@ UCN answers semantic questions about a codebase from a tree-sitter index: where
19
19
  2. `ucn find <name>` pins a definition. Pass its `path:line:name` handle to every later command. A plain name that matches several definitions carries an ambiguity warning in `show`, `source`, `trace`, `impact`, `tests`, `check` and `plan`; never treat the auto-selected definition as the repository-wide target.
20
20
  3. `ucn show <handle>` gives the summary, callers and callees. `--sections` replaces that selection; for example, `--sections=summary,source,tests` requests those three sections.
21
21
  4. Before a change: `ucn impact <handle>` and `ucn tests <handle> --depth=3`.
22
- 5. After a signature change: `ucn check <handle>`; before committing: target-less `ucn check`. Both compare a changed declaration with its parameters at `--base` (default `HEAD`), so the call sites bound to the old declaration are checked against the new one; a site that fits no remaining overload is a mismatch, never an excluded other-target line. Target-less `check` gives each changed symbol a verdict by what the change did. `BLOCKED`: a break was observed (a call site that no longer fits, an import or bare call of a deleted name, an import of a name its module no longer exports, an import of a deleted file, an object literal declared as a type that gained a member it lacks); every site is listed. A site the toolchain does not look at (a line under `// @ts-ignore` or `# type: ignore`, a module loaded by a call the compiler does not resolve) is listed and is `REVIEW`, never what blocks. `REVIEW`: the change can reach existing code and the engine cannot settle it (a changed header, generator kind or body-inferred return; an added name already in use or a member that overrides, is required, is a new member of a TypeScript class other code can hold or enumerate, is an operator or conversion, or is invoked by a runtime or framework; a deletion a shield holds; a `(module)` item for changed exports, removed imports, loose module code or a new file an existing import can resolve to), with the reason named. `READY`: nothing that existed depends on it (a body edit that leaves the contract alone, new code under new names, a deletion with no static dependents). `TRUST` is the worst verdict. `--staged` describes the git index, not the worktree: files, `.gitignore` and `.ucn.json` come from the index, including paths marked skip-worktree or assume-unchanged; when the index cannot be exported, `check` and `impact` decline.
22
+ 5. After a signature change: `ucn check <handle>`; before committing: target-less `ucn check`. Both compare a changed declaration with its parameters at `--base` (default `HEAD`), so the call sites bound to the old declaration are checked against the new one; a site that fits no remaining overload is a mismatch, never an excluded other-target line. Target-less `check` gives each changed symbol a verdict by what the change did. `BLOCKED`: a break was observed (a call site that no longer fits, an import or bare call of a deleted name, an import of a name its module no longer exports, an import of a deleted file, an object literal declared as a type that gained a member it lacks); every site is listed. A site the toolchain does not look at (a line under `// @ts-ignore` or `# type: ignore`, a module loaded by a call the compiler does not resolve) is listed and is `REVIEW`, never what blocks. `REVIEW`: the change can reach existing code and the engine cannot settle it (a changed header, generator kind or body-inferred return; an added name already in use or a member that overrides, is required, is a new member of a TypeScript class other code can hold or enumerate, is an operator or conversion, or is invoked by a runtime or framework; a deletion a shield holds; a `(module)` item for changed exports, removed imports, loose module code or a new file an existing import can resolve to), with the reason named. `READY`: the modeled change checks found no unresolved exposure (a body edit that leaves the contract alone and preserves its call requirements, independent new code, or a deletion with no static dependents). Added or changed calls to project declarations are checked too: a proven argument-count mismatch blocks, while an unresolved target or call interface requires review. Changes to checker directives can require review even when declaration tokens stay the same. READY is not a compiler or runtime correctness claim. `TRUST` is the worst verdict. `--staged` describes the git index, not the worktree: files, `.gitignore` and `.ucn.json` come from the index, including paths marked skip-worktree or assume-unchanged; when the index cannot be exported, `check` and `impact` decline.
23
23
  6. `ucn source <handle>` or `ucn source path/to/file:10-30` for exact code, only when inspection is needed.
24
24
 
25
25
  The direct tests in `show <handle> --sections=tests` match `tests <handle>`. Equivalent file and line parameters preserve the same exact declaration, including overloads.
@@ -107,6 +107,8 @@ A test is a unit: its file, the suites around it, its name and its line, read fr
107
107
 
108
108
  C, C++ and HTML tests are not read as units. Not read anywhere: what a package's code does (an installed pytest plugin, a vite plugin called in a configuration, a preset, a runner or an extension a test class names, the value a package's function gives back as a row), what a fixture or a setup reaches more than one project module away, what a function of another project module gives back as a row, a constructor one module writes on a class of another as seen from that other module, a using alias of a constructed type, an array or a tuple, and setup files a runner loads for every file (jest setup files, mocha root hooks). Type names are resolved on two assumptions: the code compiles (a name has one meaning where it is used), and the packages and namespaces the project declares types in, and the namespaces around those, hold the types its source declares. Where the runner's options are not read the tests are listed as declared, with no command.
109
109
 
110
+ Direct and transitive test links keep the selected declaration and the physical call position, including several functions or tests on one line. Text output includes each available unit selector as `run test:`; a position that cannot distinguish neighboring units withholds an exact selector. Rust tests introduced by conditional attributes stay visible, with commands withheld when their configuration is unresolved.
111
+
110
112
  `check` selects tests for every changed symbol by its declaration, counts each test once (`Tests potentially affected: N in M files`, `at least N` and `(K conditional)` as above) and prints the runner's command under `Run tests:`.
111
113
 
112
114
  Numeric evidence values are ordinal ranking weights, not probabilities. An observed-text zero is not a semantic zero or safe-delete proof.
@@ -158,7 +160,7 @@ C/C++ uses `compile_commands.json` when present for header classification and in
158
160
 
159
161
  ## Deletion protocol
160
162
 
161
- `ucn plan <handle> --delete` answers for one declaration: `no-static-dependents`, `has-static-dependents` with the first use, or `review` with the reason. It applies the rule `deadcode` applies to every name and is static evidence, not a guarantee. `deadcode` is a candidate generator, never deletion proof. Before deleting, inspect `usages`, `impact`, `entrypoints`, `api`, and `repo --sections=health --deep`, then corroborate with the compiler/type checker and tests. Members that reflection (exact names or literal-fragment patterns), computed dispatch, runtime protocols (Python dunders, Java serialization callbacks, C# pattern members, JS `toJSON`/`then`), unknown decorators, event-handler assignment, an outside base class, or macro expansion may reach are withheld or warned; the `deadcode` notes in the command reference list them. Never delete solely from `deadcode` or an observed-text-zero result.
163
+ `ucn plan <handle> --delete` answers for one declaration: `no-static-dependents`, `has-static-dependents` with the first use, or `review` with the reason. It applies the rule `deadcode` applies to every name and is static evidence, not a guarantee. `deadcode` is a candidate generator, never deletion proof. Before deleting, inspect `usages`, `impact`, `entrypoints`, `api`, and `repo --sections=health --deep`, then corroborate with the compiler/type checker and tests. Members that reflection (exact names or literal-fragment patterns), computed dispatch, runtime protocols (Python dunders, Java serialization callbacks, C# pattern members, JS `toJSON`/`then`), unknown decorators, event-handler assignment, an outside base class, or macro expansion may reach are withheld or warned; the `deadcode` notes in the command reference list them. Known Python packaging entrypoints and required mock-patch targets also withhold deletion; ordinary strings and creation-permitted patching do not prove a dependent. Builtin decorator and base exemptions require binding identity, so aliases, shadowing and possible mutation can require review. Explicit project namespace effects are shared across files. Unknown ownership, computed namespace members and unmodeled execution order can conservatively withhold builtin exemptions project-wide (`builtin-namespace-effect` or `external-contract`), without claiming a confirmed use. Python source spellings that normalize to another identifier are withheld with `identifier-normalization` where the caller graph cannot establish their identity; runtime strings keep their exact spelling. Never delete solely from `deadcode` or an observed-text-zero result.
162
164
 
163
165
  ## Efficient use
164
166
 
@@ -22,7 +22,7 @@ Use text search (grep/ripgrep) for simple literals, error messages, configuratio
22
22
  | `impact [handle]` | Direct impact of one symbol; without a handle, the Git diff (`--base=<ref>`, `--staged`). Property targets add an access-site band, type targets a reference-site band. |
23
23
  | `tests <handle\|file>` | Statically linked direct tests; `--depth=N` adds transitively affected tests; `--calls-only` keeps direct calls and test-case matches. |
24
24
  | `check [handle]` | Call-site validation for one symbol against its declaration at `--base` (default `HEAD`); without a handle, the composed pre-commit check of the diff (`--base`, `--staged`). |
25
- | `plan <handle>` | Preview `--rename-to=X`, `--add-param=p` (`--default-value=v`), or `--remove-param=p`. Never edits files. A Python param added beside `*args` or keyword-only params is keyword-only (after them, before `**kwargs`); calls passing arguments where a param is inserted before a rest param are listed. `--delete` assesses deleting the declaration with the rule `deadcode` applies to every name: `no-static-dependents` (no use in project source outside its own declarations and no shield applies), `has-static-dependents` (the name is called or used outside the declaration; the first site is printed; evidence is by name, as in `deadcode`), or `review` with the reason (exported from a library, bound to an external or project contract, registered, reflected, source that could not be searched, used only inside the declaration itself). Static evidence, not a guarantee: run the compiler and the tests. `"kind": "application"` in `.ucn.json` lets exported symbols be assessed. |
25
+ | `plan <handle>` | Preview `--rename-to=X`, `--add-param=p` (`--default-value=v`), or `--remove-param=p`. Never edits files. A Python param added beside `*args` or keyword-only params is keyword-only (after them, before `**kwargs`); calls passing arguments where a param is inserted before a rest param are listed. `--delete` assesses deleting the declaration with the rule `deadcode` applies to every name: `no-static-dependents` (no use in project source outside its own declarations and no shield applies), `has-static-dependents` (a call proven to reach this declaration, or an unambiguous use of its name, exists outside the declaration; the first site is printed), or `review` with the reason (exported from a library, bound to an external or project contract, registered, named by packaging metadata, reflected or named by a required mock-patch target, uses of the name that are not proven to reach this declaration, source that could not be searched, used only inside the declaration itself). Static evidence, not a guarantee: run the compiler and the tests. `"kind": "application"` in `.ucn.json` lets exported symbols be assessed. |
26
26
 
27
27
  `show <handle> --sections=tests` and `tests <handle>` select direct tests for the same declaration. Equivalent `--file` and `--line` parameters preserve that pin, including overloaded methods.
28
28
 
package/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  One section per released version, written from the commit subjects by `scripts/changelog.js` when the version is cut.
4
4
 
5
+ ## 5.6.1 - 2026-11-14
6
+
7
+ ### Fixes
8
+
9
+ - deletion verdicts hold for mock-patch targets and packaging entry points and cite only proven uses, check judges the calls a change adds, a change selects tests by its declaration, and the pre-commit check repeats less work (525369e)
10
+
11
+ ### Documentation
12
+
13
+ - the README reports the check and test-unit matrices at their current size, 507 single changes and 344 test projects (30923d8)
14
+ - the README's ripgrep example states only what its animation shows (8e51ac2)
15
+
5
16
  ## 5.6.0 - 2026-11-14
6
17
 
7
18
  ### Features
package/README.md CHANGED
@@ -101,11 +101,8 @@ receiver and ownership evidence determine how the candidate is classified.
101
101
 
102
102
  In ripgrep at [`82313cf9`](https://github.com/BurntSushi/ripgrep/commit/82313cf95849bfe425109ad9506a52154879b1b1),
103
103
  the selected `file_name` helper has four confirmed call sites among the 29 lines
104
- that mention the name. The other 25 are accounted for as well: 17 are not calls
105
- and 8 are attributed to other targets with the same name. This animation is a
106
- capture from September 2026, when one of those eight was still an unverified
107
- candidate. It shows the same 29 lines alongside import relationships and
108
- same-name definitions drawn from UCN's output.
104
+ that mention the name. This animation accounts for all 29 lines, alongside
105
+ import relationships and same-name definitions drawn from UCN's output.
109
106
 
110
107
  <a href="assets/readme/ucn-evidence-graph.gif"><img src="assets/readme/ucn-evidence-graph.gif" width="736" alt="Captured ripgrep findings for file_name: 29 matching lines in six files, partitioned into 4 confirmed calls, 1 unverified candidate, 17 non-call lines, 7 other-target lines, and 0 unaccounted. The graph shows selected import relationships and the distinct definitions; motion is illustrative."></a>
111
108
 
@@ -157,9 +154,9 @@ claims; 4 additional claims could not be pinned by the oracle and were unscored.
157
154
  The release gate also runs three matrices of small projects written by the
158
155
  evaluation, each judged by the language's own compiler or test runner. In the
159
156
  same run, across 68 deletions, `plan --delete` reported `no-static-dependents`
160
- for none that a compiler rejects. Across 369 single changes, `check` called
161
- none ready that a compiler rejects and blocked none that it accepts. Of 338
162
- test projects, 333 were judged by their own test runners, with zero failures
157
+ for none that a compiler rejects. Across 507 single changes, `check` called
158
+ none ready that a compiler rejects and blocked none that it accepts. Of 344
159
+ test projects, 339 were judged by their own test runners, with zero failures
163
160
  in the matrix's test-discovery and command-selection checks. These checks
164
161
  account for skipped and conditional tests and each runner's selection scope;
165
162
  five projects were not runner-judged.
package/core/analysis.js CHANGED
@@ -116,7 +116,7 @@ function tagInTestCase(index, sites) {
116
116
  const classify = kindOf.get(fileEntry.language);
117
117
  if (classify && site.callerFile && site.callerStartLine != null) {
118
118
  const encl = index.findEnclosingFunction
119
- ? index.findEnclosingFunction(site.callerFile, site.line, true)
119
+ ? index.findEnclosingFunction(site.callerFile, site.line, true, site)
120
120
  : null;
121
121
  if (encl && classify(encl) === 'test') {
122
122
  site.inTestCase = true;
@@ -124,7 +124,7 @@ function tagInTestCase(index, sites) {
124
124
  }
125
125
  }
126
126
  if (site.line != null && isTestFile(fileEntry.relativePath, fileEntry.language) &&
127
- model.unitAt(sitePath, site.line)) {
127
+ model.unitAt(sitePath, site)) {
128
128
  site.inTestCase = true;
129
129
  }
130
130
  }
@@ -2639,7 +2639,7 @@ function diffImpact(index, options = {}) {
2639
2639
  // A deleted declaration's name is assessed as a deletion.
2640
2640
  const removedNames = moved.removedNames.filter(name => !gone.has(name));
2641
2641
  if (removedNames.length > 0 || moved.importsRemoved > 0 || moved.importsAdded > 0 ||
2642
- moved.codeChanged || moved.starExportRemoved || moved.reboundBindings.length > 0) {
2642
+ moved.codeChanged || moved.checkingChanged || moved.starExportRemoved || moved.reboundBindings.length > 0) {
2643
2643
  moduleChanges.push({
2644
2644
  filePath: change.filePath,
2645
2645
  relativePath: change.relativePath,
package/core/cache.js CHANGED
@@ -1015,7 +1015,9 @@ function clearAllCaches() {
1015
1015
  // every file's text on each query).
1016
1016
  // v335: reflection/eval facts follow commented and parenthesized callees;
1017
1017
  // package reflection qualifiers retain Unicode names and lexical ownership.
1018
- const CACHE_FORMAT_VERSION = 335;
1018
+ // v336 (fixes #501/#503): binding-resolved Python registration/reflection,
1019
+ // nested runtime owner paths, JS computed calls, and owned Rust attributes.
1020
+ const CACHE_FORMAT_VERSION = 336;
1019
1021
  const USAGE_CACHE_FILE = 'usage-results.json';
1020
1022
 
1021
1023
  function usageResultKey(index, filePath, name, mode) {
@@ -0,0 +1,130 @@
1
+ /** Positive, deliberately bounded argument-fit proofs for changed calls. */
2
+ 'use strict';
3
+
4
+ const { langTraits } = require('../languages');
5
+ const { argumentProvablyConverts, cppArgumentProvablyConverts } = require('./callers');
6
+
7
+ function literalKind(argument) {
8
+ if (argument?.kind === 'number_literal' && /^\d+$/.test(argument.text || '') &&
9
+ Number(argument.text) <= 127) return 'smallInteger';
10
+ // Prefixes can change the type (Python bytes, C++ encoded strings).
11
+ // Only plain quoted strings belong to this bounded proof domain.
12
+ if (argument?.kind === 'string_literal' && ['"', "'"].includes(argument.text?.[0])) return 'string';
13
+ if (['true', 'false', 'boolean_literal'].includes(argument?.nodeType)) return 'boolean';
14
+ return null;
15
+ }
16
+
17
+ /** A written builtin type cannot borrow a project or imported namesake. */
18
+ function unshadowed(index, definition, type) {
19
+ const simple = String(type || '').replace(/^&/, '').trim();
20
+ if ((index.symbols.get(simple) || []).length) return false;
21
+ const entry = index.files.get(definition.file);
22
+ if ((entry?.importDetails || []).some(detail => detail.type === 'from' && detail.names?.includes('*'))) return false;
23
+ return !(entry?.importBindings || []).some(binding => binding.name === '*' || binding.kind === 'wildcard' ||
24
+ binding.name === simple || binding.local === simple || binding.alias === simple);
25
+ }
26
+
27
+ function unavailableSurface(index, definition, caller) {
28
+ const sameOwner = caller.file === definition.file && caller.className && caller.className === definition.className &&
29
+ (caller.namespace || null) === (definition.namespace || null) &&
30
+ (caller.enclosingType || null) === (definition.enclosingType || null);
31
+ const uncertain = member => member.isAbstract || (member.modifiers || []).includes('abstract') || (member.decorators || []).length > 0 ||
32
+ !sameOwner && (member.modifiers || []).some(modifier => ['private', 'protected'].includes(modifier));
33
+ if (uncertain(definition)) return true;
34
+ if (definition.initializerOf) {
35
+ const owners = (index.symbols.get(definition.initializerOf) || []).filter(owner => owner.type === 'class');
36
+ if (owners.length !== 1 || uncertain(owners[0])) return true;
37
+ }
38
+ if (definition.type !== 'class') return false;
39
+ const entry = index.files.get(definition.file);
40
+ return (entry?.symbols || []).some(member => member.className === definition.name &&
41
+ member.startLine >= definition.startLine && member.startLine <= definition.endLine &&
42
+ (member.type === 'constructor' || member.isConstructor ||
43
+ ['constructor', '__init__'].includes(member.name)) && uncertain(member));
44
+ }
45
+
46
+ /** A visible checked-JS/JSDoc contract withdraws the dynamic-only proof. */
47
+ function typedComments(index, file) {
48
+ const trait = langTraits(index.files.get(file)?.language)?.checkTypedComment;
49
+ if (!trait) return false;
50
+ const memo = index._opMemo?.('checkTypedComments', () => new Map());
51
+ if (memo?.has(file)) return memo.get(file);
52
+ let found = true;
53
+ try {
54
+ const source = index._readFile(file);
55
+ const root = index._getParsedTree(file, source, index.files.get(file)?.language)?.rootNode;
56
+ if (root) found = root.descendantsOfType('comment').some(comment => trait(comment.text));
57
+ } catch (_) { /* unread checking context stays unknown */ }
58
+ memo?.set(file, found);
59
+ return found;
60
+ }
61
+
62
+ /** True is a proof for this small domain; false means review, not mismatch. */
63
+ function positivelyFits(index, definition, caller, record, result, stableInvocation = false) {
64
+ // A changed call may reuse its baseline availability only under an
65
+ // unchanged invocation context. Count/type fit alone cannot prove a
66
+ // new invocation, a receiver/generic/passing-mode change, or its result
67
+ // use valid. Constructors and their implementation projections are
68
+ // outside this proof: the public overload interface is not the body.
69
+ if (!stableInvocation || definition.initializerOf || definition.isConstructor ||
70
+ ['class', 'struct', 'record', 'constructor'].includes(definition.type)) return false;
71
+ if (unavailableSurface(index, definition, caller) || (definition.decorators || []).length) return false;
72
+ // A literal may select a different overload with different access,
73
+ // exception or passing-mode requirements even when both return the
74
+ // same type. Family completeness is not proved here: only a unique
75
+ // project declaration belongs to this small positive domain.
76
+ if ((index.symbols.get(definition.name) || []).length !== 1) return false;
77
+ if (definition.className) {
78
+ // A receiver or an inherited outside overload can choose a method
79
+ // absent from the project family. Only a static member of a
80
+ // non-derived owner belongs to this bounded preservation proof.
81
+ if (!(definition.modifiers || []).includes('static')) return false;
82
+ const owners = (index.symbols.get(definition.className) || []).filter(owner => owner.file === definition.file &&
83
+ owner.startLine <= definition.startLine && owner.endLine >= definition.endLine);
84
+ if (owners.length !== 1 || owners[0].extends?.length || owners[0].implements?.length) return false;
85
+ }
86
+ const traits = langTraits(index.files.get(definition.file)?.language);
87
+ if ((definition.modifiers || []).some(modifier => (traits.compileTimeCallableMarkers || []).includes(modifier))) return false;
88
+ const callerTraits = langTraits(index.files.get(caller.file)?.language);
89
+ // Written TS declarations do not make ordinary JS arguments checked.
90
+ if (callerTraits.lenientCallArity) return !typedComments(index, caller.file) && !typedComments(index, definition.file);
91
+ const shape = result.callShapes?.[0];
92
+ if (!shape || shape.hasSpread || shape.indirectKind) return false;
93
+ const arguments_ = shape.argumentFacts || [];
94
+ if (arguments_.length === 0) return false;
95
+ if (arguments_.length !== shape.argCount) return false;
96
+ const lists = result.parameterLists || [];
97
+ if (lists.length !== 1) return false;
98
+ const language = index.files.get(definition.file)?.language;
99
+ return lists.some(parameters => {
100
+ let positional = 0;
101
+ return arguments_.every((argument, at) => {
102
+ const parameter = argument.keyword ? parameters.find(p => p.name === argument.keyword)
103
+ : parameters[positional++];
104
+ if (!parameter || parameter.rest || !parameter.type) return false;
105
+ // The nominal conversion helpers classify values, not C++
106
+ // lvalue/rvalue reference binding or language passing modes.
107
+ if (/[&*]/.test(parameter.type) && parameter.type !== '&str') return false;
108
+ const kind = literalKind(argument);
109
+ if (!kind) return false;
110
+ if (traits.checkLiteralArguments) {
111
+ const accepted = traits.checkLiteralArguments[kind] || [];
112
+ return accepted.includes(parameter.type) && unshadowed(index, definition, parameter.type);
113
+ }
114
+ // Reuse the nominal overload engine's positive conversion
115
+ // facts, never its weaker "not disproved" applicability test.
116
+ if (!traits.hasArityOverloads || parameter.type.includes('.') ||
117
+ !unshadowed(index, definition, parameter.type)) return false;
118
+ const argumentKind = record.argKinds?.[at];
119
+ if (!argumentKind) return false;
120
+ if (traits.userDefinedConversions === 'constructors-and-operators') {
121
+ return cppArgumentProvablyConverts(index, argumentKind, parameter, definition,
122
+ { ...record, argumentFile: caller.file }, at);
123
+ }
124
+ return argumentProvablyConverts(index, argumentKind, parameter, language, definition,
125
+ { file: caller.file });
126
+ });
127
+ });
128
+ }
129
+
130
+ module.exports = { positivelyFits };
package/core/callers.js CHANGED
@@ -32574,6 +32574,6 @@ function findCallbackUsages(index, name) {
32574
32574
  return usages;
32575
32575
  }
32576
32576
 
32577
- module.exports = { rustPathQualifierVerdict: _pathQualifierVerdict, rustTraitDeclPathProbe: _rustTraitDeclPathProbe, _recordIsNoCall, _cppMemberUsingBases, _cppTargetVisibility, rustTypeNameDenotes, _textualIncludeClosures, _textualLinkVerdict, isProvenanceBuiltinReceiver, provenanceParameterIdentity: _overloadTypeIdentity, selectProvenanceOverload, _unresolvedModuleIsGap, _nonSourceModuleFile, _moduleValueAliasType, _importReaches, _sameNominalPackageDir, getCachedCalls, getCachedCallsBearing, findCallers, findCallees, getInstanceAttributeTypes, findCallbackUsages, _nameBindingReaches, _moduleAttributeBindingReaches, _declaredFieldType, _projectTopLevelNames, _callArityCompatible, _closeCallableIdentityGroup, _overloadDiscipline, _overloadApplicable, _buildReturnTypeFlowMap, _lookupReturnTypeFlow, _cFamilySignatureKey,
32577
+ module.exports = { argumentProvablyConverts: _argProvablyConverts, cppArgumentProvablyConverts: _cppArgProvablyConverts, rustPathQualifierVerdict: _pathQualifierVerdict, rustTraitDeclPathProbe: _rustTraitDeclPathProbe, _recordIsNoCall, _cppMemberUsingBases, _cppTargetVisibility, rustTypeNameDenotes, _textualIncludeClosures, _textualLinkVerdict, isProvenanceBuiltinReceiver, provenanceParameterIdentity: _overloadTypeIdentity, selectProvenanceOverload, _unresolvedModuleIsGap, _nonSourceModuleFile, _moduleValueAliasType, _importReaches, _sameNominalPackageDir, getCachedCalls, getCachedCallsBearing, findCallers, findCallees, getInstanceAttributeTypes, findCallbackUsages, _nameBindingReaches, _moduleAttributeBindingReaches, _declaredFieldType, _projectTopLevelNames, _callArityCompatible, _closeCallableIdentityGroup, _overloadDiscipline, _overloadApplicable, _buildReturnTypeFlowMap, _lookupReturnTypeFlow, _cFamilySignatureKey,
32578
32578
  _isConfigurationAlternative, _isCrossFileConfigurationMember, _configurationAlternativeFiles, _sameOwnerPath,
32579
32579
  _goFileConstraint, _typeQueryRecords };
package/core/capture.js CHANGED
@@ -63,7 +63,8 @@ const HAZARD_OF_SHIELD = Object.freeze({
63
63
  * name in a shared scope) package, an inclusion can reach it
64
64
  * from anywhere.
65
65
  *
66
- * Returns a predicate over (relative path, line), or null for "anywhere".
66
+ * Returns candidate files and a predicate over (relative path, line), or
67
+ * null for "anywhere". Candidate files bound the scan before parsing.
67
68
  * An occurrence elsewhere is another name that happens to be spelled the
68
69
  * same: it could not have meant this declaration before, and cannot now.
69
70
  */
@@ -73,8 +74,8 @@ function reachOf(index, definition) {
73
74
  if (!entry) return null;
74
75
  const own = entry.relativePath;
75
76
  if (definition.lexicalScopeStartLine != null) {
76
- return (file, line) => file === own &&
77
- line >= definition.lexicalScopeStartLine && line <= definition.lexicalScopeEndLine;
77
+ return { files: new Set([own]), includes: (file, line) => file === own &&
78
+ line >= definition.lexicalScopeStartLine && line <= definition.lexicalScopeEndLine };
78
79
  }
79
80
  const traits = langTraits(entry.language) || {};
80
81
  if (traits.packageScope !== 'file' || traits.textualIncludes) return null;
@@ -87,7 +88,7 @@ function reachOf(index, definition) {
87
88
  for (const symbol of entry.symbols || []) if (!symbol.className) exported.add(symbol.name);
88
89
  }
89
90
  // A name the module does not export stays in the module.
90
- if (!exported.has(definition.name)) return file => file === own;
91
+ if (!exported.has(definition.name)) return { files, includes: file => file === own };
91
92
  for (const other of index.files.values()) {
92
93
  const resolved = other.moduleResolved || {};
93
94
  const takesAll = (other.importDetails || []).some(detail =>
@@ -104,7 +105,7 @@ function reachOf(index, definition) {
104
105
  binding.kind === 'namespace' || binding.kind === 'default'));
105
106
  if (takesAll || binds) files.add(other.relativePath);
106
107
  }
107
- return file => files.has(file);
108
+ return { files, includes: file => files.has(file) };
108
109
  }
109
110
 
110
111
  /**
@@ -119,13 +120,19 @@ function reachOf(index, definition) {
119
120
  * @returns {{outside: {file, line}[], total: number}}
120
121
  */
121
122
  function occurrencesOutsideChange(index, name, addedLinesByFile, definition = null) {
122
- const rows = index.usages(name, { includeTests: true, codeOnly: true }) || [];
123
123
  const reaches = reachOf(index, definition);
124
+ const options = { includeTests: true, codeOnly: true };
125
+ // Use the same proven module/lexical reach that filters the answer to
126
+ // avoid parsing unrelated files for every added declaration. The caller
127
+ // still assesses the full source-universe account before claiming novelty.
128
+ const rows = reaches
129
+ ? [...reaches.files].sort().flatMap(file => index.usages(name, { ...options, file }) || [])
130
+ : index.usages(name, options) || [];
124
131
  const outside = [];
125
132
  for (const row of rows) {
126
133
  const file = row.relativePath || row.file;
127
134
  if (addedLinesByFile.get(file)?.has(row.line)) continue;
128
- if (reaches && !reaches(file, row.line)) continue;
135
+ if (reaches && !reaches.includes(file, row.line)) continue;
129
136
  outside.push({ file, line: row.line });
130
137
  }
131
138
  outside.sort((a, b) => (a.file === b.file ? a.line - b.line : (a.file < b.file ? -1 : 1)));
@@ -139,6 +146,31 @@ function ownersOf(index, definition) {
139
146
  type.startLine <= definition.startLine && (type.endLine || type.startLine) >= definition.startLine);
140
147
  }
141
148
 
149
+ /** A member of a wholly new, unreachable owner cannot capture a namesake. */
150
+ function newOwnerUnreached(index, definition, addedLinesByFile) {
151
+ if (!definition.className) return false;
152
+ const owners = ownersOf(index, definition);
153
+ if (owners.length !== 1) return false;
154
+ const owner = owners[0];
155
+ // An impl block, namespace or out-of-class member group can be newly
156
+ // written while its type existed all along (including outside source).
157
+ if (!['class', 'struct', 'record', 'enum', 'interface', 'trait', 'type', 'union'].includes(owner.type)) return false;
158
+ const memo = index._opMemo?.('checkNewOwnerReach', () => new WeakMap());
159
+ if (memo?.has(owner)) return memo.get(owner);
160
+ let unreachable = false;
161
+ const added = addedLinesByFile.get(owner.relativePath);
162
+ if (added?.has(owner.nameLine || owner.startLine) && !owner.extends && !owner.implements &&
163
+ !(owner.decorators || []).length && !(owner.attributes || []).length) {
164
+ // Name reach is the module/import scope of THIS owner, not the
165
+ // global spelling of a field. Bases, registrations and reflection
166
+ // remain separately guarded by the ordinary capture assessment.
167
+ const occurrences = occurrencesOutsideChange(index, owner.name, addedLinesByFile, owner);
168
+ unreachable = occurrences.outside.length === 0;
169
+ }
170
+ memo?.set(owner, unreachable);
171
+ return unreachable;
172
+ }
173
+
142
174
  /** One syntax tree per file for the questions below. */
143
175
  function treeReader(index) {
144
176
  const { parseRevision } = require('./change-kinds');
@@ -328,4 +360,4 @@ function captureHazards(index, definition, facts = {}) {
328
360
  return hazards;
329
361
  }
330
362
 
331
- module.exports = { occurrencesOutsideChange, captureHazards, shapeChangeOf, literalsLackingMember, HAZARD_TEXT };
363
+ module.exports = { occurrencesOutsideChange, captureHazards, shapeChangeOf, literalsLackingMember, newOwnerUnreached, HAZARD_TEXT };