gitnexus 1.6.6-rc.48 → 1.6.6-rc.49
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/dist/core/ingestion/pipeline.js +2 -0
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.d.ts +2 -0
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +78 -8
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.d.ts +4 -1
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +43 -1
- package/dist/core/ingestion/scope-resolution/pipeline/phase.d.ts +3 -0
- package/dist/core/ingestion/scope-resolution/pipeline/phase.js +6 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +7 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.js +11 -1
- package/dist/core/ingestion/scope-resolution/resolution-outcome.d.ts +24 -0
- package/dist/core/ingestion/scope-resolution/resolution-outcome.js +1 -0
- package/dist/types/pipeline.d.ts +7 -0
- package/package.json +1 -1
|
@@ -62,6 +62,7 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
|
|
|
62
62
|
const { totalFiles, usedWorkerPool } = getPhaseOutput(results, 'parse');
|
|
63
63
|
let communityResult;
|
|
64
64
|
let processResult;
|
|
65
|
+
const resolutionOutcomes = getPhaseOutput(results, 'scopeResolution').resolutionOutcomes;
|
|
65
66
|
if (!options?.skipGraphPhases) {
|
|
66
67
|
communityResult = getPhaseOutput(results, 'communities').communityResult;
|
|
67
68
|
processResult = getPhaseOutput(results, 'processes').processResult;
|
|
@@ -84,6 +85,7 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
|
|
|
84
85
|
totalFileCount: totalFiles,
|
|
85
86
|
communityResult,
|
|
86
87
|
processResult,
|
|
88
|
+
resolutionOutcomes,
|
|
87
89
|
usedWorkerPool,
|
|
88
90
|
};
|
|
89
91
|
};
|
|
@@ -23,6 +23,7 @@ import type { SemanticModel } from '../../model/semantic-model.js';
|
|
|
23
23
|
import type { WorkspaceResolutionIndex } from '../workspace-index.js';
|
|
24
24
|
import type { GraphNodeLookup } from '../graph-bridge/node-lookup.js';
|
|
25
25
|
import type { ScopeResolver } from '../contract/scope-resolver.js';
|
|
26
|
+
import type { ResolutionOutcomeRecorder } from '../resolution-outcome.js';
|
|
26
27
|
import { type ConversionRankFn } from './overload-narrowing.js';
|
|
27
28
|
export declare function emitFreeCallFallback(graph: KnowledgeGraph, scopes: ScopeResolutionIndexes, parsedFiles: readonly ParsedFile[], nodeLookup: GraphNodeLookup, _referenceIndex: {
|
|
28
29
|
readonly bySourceScope: ReadonlyMap<ScopeId, readonly Reference[]>;
|
|
@@ -51,6 +52,7 @@ export declare function emitFreeCallFallback(graph: KnowledgeGraph, scopes: Scop
|
|
|
51
52
|
* fail at the call site. Three-valued; `'unknown'` keeps the
|
|
52
53
|
* candidate (monotonicity). */
|
|
53
54
|
readonly constraintCompatibility?: ScopeResolver['constraintCompatibility'];
|
|
55
|
+
readonly recordResolutionOutcome?: ResolutionOutcomeRecorder;
|
|
54
56
|
}): number;
|
|
55
57
|
/** Walk up from the call-site scope to the enclosing class scope,
|
|
56
58
|
* pick a method member by name with overload narrowing on arity +
|
|
@@ -86,7 +86,18 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
|
|
|
86
86
|
// Cross-file candidates are shadowing; keep first-match.
|
|
87
87
|
const sameFile = narrowed.every((d) => d.filePath === narrowed[0].filePath);
|
|
88
88
|
if (sameFile) {
|
|
89
|
-
|
|
89
|
+
recordSuppressedOutcome(options.recordResolutionOutcome, {
|
|
90
|
+
phase: 'free-call-fallback',
|
|
91
|
+
filePath: parsed.filePath,
|
|
92
|
+
name: site.name,
|
|
93
|
+
range: site.atRange,
|
|
94
|
+
reason: suppressionReasonForOverload(narrowed, site.arity, {
|
|
95
|
+
conversionRankFn: options.conversionRankFn,
|
|
96
|
+
argumentTypes: site.argumentTypes,
|
|
97
|
+
}),
|
|
98
|
+
candidates: narrowed,
|
|
99
|
+
});
|
|
100
|
+
handledSites.add(siteKey(parsed.filePath, site));
|
|
90
101
|
continue;
|
|
91
102
|
}
|
|
92
103
|
}
|
|
@@ -109,7 +120,19 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
|
|
|
109
120
|
argumentTypes: site.argumentTypes,
|
|
110
121
|
atRange: { startLine: site.atRange.startLine, startCol: site.atRange.startCol },
|
|
111
122
|
}, parsed, scopes, parsedFiles);
|
|
112
|
-
const
|
|
123
|
+
const key = siteKey(parsed.filePath, site);
|
|
124
|
+
if (adlSuppressed && ordinary.length === 0) {
|
|
125
|
+
recordSuppressedOutcome(options.recordResolutionOutcome, {
|
|
126
|
+
phase: 'free-call-fallback',
|
|
127
|
+
filePath: parsed.filePath,
|
|
128
|
+
name: site.name,
|
|
129
|
+
range: site.atRange,
|
|
130
|
+
reason: 'adl-ordinary-lookup-blocked',
|
|
131
|
+
candidates: ordinary,
|
|
132
|
+
});
|
|
133
|
+
handledSites.add(key);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
113
136
|
if (adl === undefined || adl.length === 0) {
|
|
114
137
|
// No ADL contribution. Default behavior: `ordinary[0]` —
|
|
115
138
|
// scope-chain walk preserves local-shadows-import precedence.
|
|
@@ -135,7 +158,7 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
|
|
|
135
158
|
fnDef = narrowed[0];
|
|
136
159
|
}
|
|
137
160
|
else if (narrowed.length === 0) {
|
|
138
|
-
handledSites.add(
|
|
161
|
+
handledSites.add(key);
|
|
139
162
|
continue;
|
|
140
163
|
}
|
|
141
164
|
else {
|
|
@@ -145,7 +168,18 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
|
|
|
145
168
|
// first-match (shadowing semantics).
|
|
146
169
|
const sameFile = narrowed.every((d) => d.filePath === narrowed[0].filePath);
|
|
147
170
|
if (sameFile) {
|
|
148
|
-
|
|
171
|
+
recordSuppressedOutcome(options.recordResolutionOutcome, {
|
|
172
|
+
phase: 'free-call-fallback',
|
|
173
|
+
filePath: parsed.filePath,
|
|
174
|
+
name: site.name,
|
|
175
|
+
range: site.atRange,
|
|
176
|
+
reason: suppressionReasonForOverload(narrowed, site.arity, {
|
|
177
|
+
conversionRankFn: options.conversionRankFn,
|
|
178
|
+
argumentTypes: site.argumentTypes,
|
|
179
|
+
}),
|
|
180
|
+
candidates: narrowed,
|
|
181
|
+
});
|
|
182
|
+
handledSites.add(key);
|
|
149
183
|
continue;
|
|
150
184
|
}
|
|
151
185
|
fnDef = ordinary[0];
|
|
@@ -174,17 +208,28 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
|
|
|
174
208
|
fnDef = narrowed[0];
|
|
175
209
|
}
|
|
176
210
|
else if (narrowed.length === 0) {
|
|
177
|
-
handledSites.add(
|
|
211
|
+
handledSites.add(key);
|
|
178
212
|
continue;
|
|
179
213
|
}
|
|
180
214
|
else if (narrowed.length > 1) {
|
|
215
|
+
recordSuppressedOutcome(options.recordResolutionOutcome, {
|
|
216
|
+
phase: 'free-call-fallback',
|
|
217
|
+
filePath: parsed.filePath,
|
|
218
|
+
name: site.name,
|
|
219
|
+
range: site.atRange,
|
|
220
|
+
reason: suppressionReasonForOverload(narrowed, site.arity, {
|
|
221
|
+
conversionRankFn: options.conversionRankFn,
|
|
222
|
+
argumentTypes: site.argumentTypes,
|
|
223
|
+
}),
|
|
224
|
+
candidates: narrowed,
|
|
225
|
+
});
|
|
181
226
|
if (isOverloadAmbiguousAfterNormalization(narrowed, site.arity)) {
|
|
182
|
-
handledSites.add(
|
|
227
|
+
handledSites.add(key);
|
|
183
228
|
continue;
|
|
184
229
|
}
|
|
185
230
|
// Multiple survivors remain after conversion-rank scoring;
|
|
186
231
|
// suppress instead of picking arbitrarily.
|
|
187
|
-
handledSites.add(
|
|
232
|
+
handledSites.add(key);
|
|
188
233
|
continue;
|
|
189
234
|
}
|
|
190
235
|
}
|
|
@@ -215,7 +260,7 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
|
|
|
215
260
|
// Always mark the site as handled — even when the dedup-collapse
|
|
216
261
|
// means we don't add a new edge — so `emit-references` skips its
|
|
217
262
|
// potentially-wrong fallback for the same site.
|
|
218
|
-
handledSites.add(
|
|
263
|
+
handledSites.add(siteKey(parsed.filePath, site));
|
|
219
264
|
const relId = `rel:CALLS:${callerGraphId}->${tgtGraphId}`;
|
|
220
265
|
if (seen.has(relId))
|
|
221
266
|
continue;
|
|
@@ -235,6 +280,31 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
|
|
|
235
280
|
}
|
|
236
281
|
return emitted;
|
|
237
282
|
}
|
|
283
|
+
function siteKey(filePath, site) {
|
|
284
|
+
return `${filePath}:${site.atRange.startLine}:${site.atRange.startCol}`;
|
|
285
|
+
}
|
|
286
|
+
function suppressionReasonForOverload(candidates, arity, ctx) {
|
|
287
|
+
if (isOverloadAmbiguousAfterNormalization(candidates, arity)) {
|
|
288
|
+
return 'overload-ambiguous-normalization';
|
|
289
|
+
}
|
|
290
|
+
if (ctx.conversionRankFn !== undefined &&
|
|
291
|
+
ctx.argumentTypes !== undefined &&
|
|
292
|
+
ctx.argumentTypes.length > 0) {
|
|
293
|
+
return 'conversion-rank-tied';
|
|
294
|
+
}
|
|
295
|
+
return 'overload-ambiguous';
|
|
296
|
+
}
|
|
297
|
+
function recordSuppressedOutcome(record, input) {
|
|
298
|
+
record?.({
|
|
299
|
+
kind: 'suppressed',
|
|
300
|
+
phase: input.phase,
|
|
301
|
+
filePath: input.filePath,
|
|
302
|
+
name: input.name,
|
|
303
|
+
range: input.range,
|
|
304
|
+
reason: input.reason,
|
|
305
|
+
candidateIds: input.candidates.map((d) => d.nodeId),
|
|
306
|
+
});
|
|
307
|
+
}
|
|
238
308
|
/**
|
|
239
309
|
* Build a `simpleName -> callable defs` index from `scopes.defs` once per
|
|
240
310
|
* pass. Mirrors the filter the old per-site scan applied: Function /
|
|
@@ -43,11 +43,14 @@ import type { SemanticModel } from '../../model/semantic-model.js';
|
|
|
43
43
|
import type { ScopeResolver } from '../contract/scope-resolver.js';
|
|
44
44
|
import type { GraphNodeLookup } from '../graph-bridge/node-lookup.js';
|
|
45
45
|
import type { WorkspaceResolutionIndex } from '../workspace-index.js';
|
|
46
|
+
import type { ResolutionOutcomeRecorder } from '../resolution-outcome.js';
|
|
46
47
|
/** Subset of `ScopeResolver` consumed by this pass. Accepting the
|
|
47
48
|
* subset rather than the full provider keeps tests and partial
|
|
48
49
|
* refactors lighter — callers only need to populate what we read. */
|
|
49
50
|
type ReceiverBoundProviderSubset = Pick<ScopeResolver, 'isSuperReceiver' | 'isSuperReceiverInContext' | 'fieldFallbackOnMethodLookup' | 'collapseMemberCallsByCallerTarget' | 'unwrapCollectionAccessor' | 'hoistTypeBindingsToModule' | 'resolveQualifiedReceiverMember' | 'resolveThisViaEnclosingClass' | 'conversionRankFn' | 'constraintCompatibility' | 'isStaticOnly'>;
|
|
50
|
-
export declare function emitReceiverBoundCalls(graph: KnowledgeGraph, scopes: ScopeResolutionIndexes, parsedFiles: readonly ParsedFile[], nodeLookup: GraphNodeLookup, handledSites: Set<string>, provider: ReceiverBoundProviderSubset, index: WorkspaceResolutionIndex, model: SemanticModel
|
|
51
|
+
export declare function emitReceiverBoundCalls(graph: KnowledgeGraph, scopes: ScopeResolutionIndexes, parsedFiles: readonly ParsedFile[], nodeLookup: GraphNodeLookup, handledSites: Set<string>, provider: ReceiverBoundProviderSubset, index: WorkspaceResolutionIndex, model: SemanticModel, options?: {
|
|
52
|
+
readonly recordResolutionOutcome?: ResolutionOutcomeRecorder;
|
|
53
|
+
}): number;
|
|
51
54
|
/**
|
|
52
55
|
* Sentinel returned by `pickOverload` when narrowing leaves >1 candidate
|
|
53
56
|
* sharing identical normalized parameter-types. Callers should suppress
|
|
@@ -84,7 +84,7 @@ function resolveClassBindingForName(scopeId, rawClassName, scopes) {
|
|
|
84
84
|
}
|
|
85
85
|
return findClassBindingInScope(scopeId, baseName, scopes);
|
|
86
86
|
}
|
|
87
|
-
export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, handledSites, provider, index, model) {
|
|
87
|
+
export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, handledSites, provider, index, model, options = {}) {
|
|
88
88
|
let emitted = 0;
|
|
89
89
|
// Per-pass dedup so the multiple cases don't double-emit if two of
|
|
90
90
|
// them resolve the same site to the same target. NEVER pre-seed
|
|
@@ -372,6 +372,15 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
|
|
|
372
372
|
if (memberDef === 'ambiguous') {
|
|
373
373
|
// Same-name ambiguity across inline-namespace children (#1564):
|
|
374
374
|
// suppress edge emission, mark site handled.
|
|
375
|
+
options.recordResolutionOutcome?.({
|
|
376
|
+
kind: 'suppressed',
|
|
377
|
+
phase: 'receiver-bound-calls',
|
|
378
|
+
filePath: parsed.filePath,
|
|
379
|
+
name: site.name,
|
|
380
|
+
range: site.atRange,
|
|
381
|
+
reason: 'inline-ns-ambiguous',
|
|
382
|
+
candidateIds: [],
|
|
383
|
+
});
|
|
375
384
|
handledSites.add(siteKey);
|
|
376
385
|
continue;
|
|
377
386
|
}
|
|
@@ -518,6 +527,7 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
|
|
|
518
527
|
const chain = [ownerDef.nodeId, ...scopes.methodDispatch.mroFor(ownerDef.nodeId)];
|
|
519
528
|
let memberDef;
|
|
520
529
|
let ambiguous = false;
|
|
530
|
+
let ambiguousOwnerId;
|
|
521
531
|
// Track whether the chain walk filtered out any static-only
|
|
522
532
|
// candidates. When it did and the chain ended with no
|
|
523
533
|
// legitimate instance member, we mark the site as handled so
|
|
@@ -549,6 +559,7 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
|
|
|
549
559
|
const picked = pickFirstNonStaticOnly(ownerId, memberName, site, model, provider);
|
|
550
560
|
if (picked === OVERLOAD_AMBIGUOUS) {
|
|
551
561
|
ambiguous = true;
|
|
562
|
+
ambiguousOwnerId = ownerId;
|
|
552
563
|
break;
|
|
553
564
|
}
|
|
554
565
|
if (picked === STATIC_ONLY_FILTERED) {
|
|
@@ -570,6 +581,7 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
|
|
|
570
581
|
// Suppress and mark handled so `emitReferencesViaLookup`
|
|
571
582
|
// doesn't re-emit the pre-resolved reference. See
|
|
572
583
|
// OVERLOAD_AMBIGUOUS docstring for the upstream cause.
|
|
584
|
+
recordReceiverOverloadSuppression(options.recordResolutionOutcome, parsed.filePath, site, ambiguousOwnerId ?? ownerDef.nodeId, memberName, model, provider);
|
|
573
585
|
handledSites.add(siteKey);
|
|
574
586
|
continue;
|
|
575
587
|
}
|
|
@@ -634,6 +646,7 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
|
|
|
634
646
|
const ownerGraphId = resolveDefGraphId(valueDef.filePath, valueDef, nodeLookup) ?? valueDef.nodeId;
|
|
635
647
|
const picked = pickOverload(ownerGraphId, memberName, site, model, provider);
|
|
636
648
|
if (picked === OVERLOAD_AMBIGUOUS) {
|
|
649
|
+
recordReceiverOverloadSuppression(options.recordResolutionOutcome, parsed.filePath, site, ownerGraphId, memberName, model, provider);
|
|
637
650
|
handledSites.add(siteKey);
|
|
638
651
|
continue;
|
|
639
652
|
}
|
|
@@ -798,3 +811,32 @@ function pickFirstNonStaticOnly(ownerId, memberName, site, model, provider) {
|
|
|
798
811
|
return OVERLOAD_AMBIGUOUS;
|
|
799
812
|
return candidates[0] ?? overloads[0];
|
|
800
813
|
}
|
|
814
|
+
function recordReceiverOverloadSuppression(record, filePath, site, ownerId, memberName, model, provider) {
|
|
815
|
+
if (record === undefined)
|
|
816
|
+
return;
|
|
817
|
+
const overloads = model.methods.lookupAllByOwner(ownerId, memberName);
|
|
818
|
+
const candidates = narrowOverloadCandidates(overloads, site.arity, site.argumentTypes, {
|
|
819
|
+
argumentTypeClasses: site.argumentTypeClasses,
|
|
820
|
+
conversionRankFn: provider.conversionRankFn,
|
|
821
|
+
constraintCompatibility: provider.constraintCompatibility,
|
|
822
|
+
});
|
|
823
|
+
const reason = isOverloadAmbiguousAfterNormalization(candidates, site.arity)
|
|
824
|
+
? 'overload-ambiguous-normalization'
|
|
825
|
+
: hasConversionRankingSignal(site, provider)
|
|
826
|
+
? 'conversion-rank-tied'
|
|
827
|
+
: 'overload-ambiguous';
|
|
828
|
+
record({
|
|
829
|
+
kind: 'suppressed',
|
|
830
|
+
phase: 'receiver-bound-calls',
|
|
831
|
+
filePath,
|
|
832
|
+
name: site.name,
|
|
833
|
+
range: site.atRange,
|
|
834
|
+
reason,
|
|
835
|
+
candidateIds: candidates.map((d) => d.nodeId),
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
function hasConversionRankingSignal(site, provider) {
|
|
839
|
+
return (provider.conversionRankFn !== undefined &&
|
|
840
|
+
site.argumentTypes !== undefined &&
|
|
841
|
+
site.argumentTypes.length > 0);
|
|
842
|
+
}
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
import type { PipelinePhase } from '../../pipeline-phases/types.js';
|
|
30
30
|
import { SupportedLanguages } from '../../../../_shared/index.js';
|
|
31
|
+
import type { ResolutionOutcome } from '../resolution-outcome.js';
|
|
31
32
|
export interface ScopeResolutionOutput {
|
|
32
33
|
/** True when at least one language ran. */
|
|
33
34
|
readonly ran: boolean;
|
|
@@ -37,6 +38,8 @@ export interface ScopeResolutionOutput {
|
|
|
37
38
|
readonly importsEmitted: number;
|
|
38
39
|
/** Reference (CALLS / ACCESSES / INHERITS / USES) edges emitted. */
|
|
39
40
|
readonly referenceEdgesEmitted: number;
|
|
41
|
+
/** Additive stream of resolver diagnostics; does not affect graph edges. */
|
|
42
|
+
readonly resolutionOutcomes: readonly ResolutionOutcome[];
|
|
40
43
|
/** Per-language breakdown for telemetry / shadow-parity. */
|
|
41
44
|
readonly perLanguage: ReadonlyMap<SupportedLanguages, {
|
|
42
45
|
readonly filesProcessed: number;
|
|
@@ -39,6 +39,7 @@ const NOOP_OUTPUT = Object.freeze({
|
|
|
39
39
|
filesProcessed: 0,
|
|
40
40
|
importsEmitted: 0,
|
|
41
41
|
referenceEdgesEmitted: 0,
|
|
42
|
+
resolutionOutcomes: [],
|
|
42
43
|
perLanguage: new Map(),
|
|
43
44
|
});
|
|
44
45
|
export const scopeResolutionPhase = {
|
|
@@ -84,6 +85,7 @@ export const scopeResolutionPhase = {
|
|
|
84
85
|
let totalImports = 0;
|
|
85
86
|
let totalRefs = 0;
|
|
86
87
|
let anyRan = false;
|
|
88
|
+
const resolutionOutcomes = [];
|
|
87
89
|
const perLanguage = new Map();
|
|
88
90
|
for (const [lang, provider] of SCOPE_RESOLVERS) {
|
|
89
91
|
if (!isRegistryPrimary(lang))
|
|
@@ -113,6 +115,9 @@ export const scopeResolutionPhase = {
|
|
|
113
115
|
treeCache: scopeTreeCache,
|
|
114
116
|
resolutionConfig,
|
|
115
117
|
preExtractedParsedFiles: preExtractedByPath,
|
|
118
|
+
recordResolutionOutcome: (outcome) => {
|
|
119
|
+
resolutionOutcomes.push(outcome);
|
|
120
|
+
},
|
|
116
121
|
onWarn: (msg) => {
|
|
117
122
|
if (isSemanticModelValidatorEnabled()) {
|
|
118
123
|
logger.warn(`[scope-resolution:${lang}] ${msg}`);
|
|
@@ -146,6 +151,7 @@ export const scopeResolutionPhase = {
|
|
|
146
151
|
filesProcessed: totalFiles,
|
|
147
152
|
importsEmitted: totalImports,
|
|
148
153
|
referenceEdgesEmitted: totalRefs,
|
|
154
|
+
resolutionOutcomes,
|
|
149
155
|
perLanguage,
|
|
150
156
|
};
|
|
151
157
|
},
|
|
@@ -27,6 +27,7 @@ import type { KnowledgeGraph } from '../../../graph/types.js';
|
|
|
27
27
|
import type { MutableSemanticModel } from '../../model/semantic-model.js';
|
|
28
28
|
import { type ResolveStats } from '../../resolve-references.js';
|
|
29
29
|
import type { ScopeResolver } from '../contract/scope-resolver.js';
|
|
30
|
+
import type { ResolutionOutcome, ResolutionOutcomeRecorder } from '../resolution-outcome.js';
|
|
30
31
|
interface RunScopeResolutionInput {
|
|
31
32
|
readonly graph: KnowledgeGraph;
|
|
32
33
|
/**
|
|
@@ -78,6 +79,11 @@ interface RunScopeResolutionInput {
|
|
|
78
79
|
* Cache miss is safe — falls back to fresh extract.
|
|
79
80
|
*/
|
|
80
81
|
readonly preExtractedParsedFiles?: ReadonlyMap<string, ParsedFile>;
|
|
82
|
+
/**
|
|
83
|
+
* Optional additive diagnostics sink. Resolver passes call this when they
|
|
84
|
+
* intentionally suppress an edge; the graph remains unchanged.
|
|
85
|
+
*/
|
|
86
|
+
readonly recordResolutionOutcome?: ResolutionOutcomeRecorder;
|
|
81
87
|
}
|
|
82
88
|
interface RunScopeResolutionStats {
|
|
83
89
|
readonly filesProcessed: number;
|
|
@@ -86,6 +92,7 @@ interface RunScopeResolutionStats {
|
|
|
86
92
|
readonly resolve: ResolveStats;
|
|
87
93
|
readonly referenceEdgesEmitted: number;
|
|
88
94
|
readonly referenceSkipped: number;
|
|
95
|
+
readonly resolutionOutcomes: readonly ResolutionOutcome[];
|
|
89
96
|
}
|
|
90
97
|
export declare function runScopeResolution(input: RunScopeResolutionInput, provider: ScopeResolver): RunScopeResolutionStats;
|
|
91
98
|
export {};
|
|
@@ -94,6 +94,11 @@ function preEmitInheritanceEdges(graph, scopes, nodeLookup) {
|
|
|
94
94
|
export function runScopeResolution(input, provider) {
|
|
95
95
|
const { graph, files } = input;
|
|
96
96
|
const onWarn = input.onWarn ?? (() => { });
|
|
97
|
+
const resolutionOutcomes = [];
|
|
98
|
+
const recordResolutionOutcome = (outcome) => {
|
|
99
|
+
resolutionOutcomes.push(outcome);
|
|
100
|
+
input.recordResolutionOutcome?.(outcome);
|
|
101
|
+
};
|
|
97
102
|
const PROF = process.env.PROF_SCOPE_RESOLUTION === '1';
|
|
98
103
|
const tStart = PROF ? process.hrtime.bigint() : 0n;
|
|
99
104
|
let fileContents;
|
|
@@ -157,6 +162,7 @@ export function runScopeResolution(input, provider) {
|
|
|
157
162
|
resolve: { sitesProcessed: 0, referencesEmitted: 0, unresolved: 0 },
|
|
158
163
|
referenceEdgesEmitted: 0,
|
|
159
164
|
referenceSkipped: 0,
|
|
165
|
+
resolutionOutcomes,
|
|
160
166
|
};
|
|
161
167
|
}
|
|
162
168
|
const tExtract = PROF ? process.hrtime.bigint() : 0n;
|
|
@@ -242,7 +248,9 @@ export function runScopeResolution(input, provider) {
|
|
|
242
248
|
const tResolve = PROF ? process.hrtime.bigint() : 0n;
|
|
243
249
|
// ── Phase 4: emit graph edges (LOAD-BEARING ORDER — see I1) ────────────
|
|
244
250
|
const handledSites = new Set(preEmittedInheritanceSites);
|
|
245
|
-
const receiverExtras = emitReceiverBoundCalls(graph, indexes, parsedFiles, nodeLookup, handledSites, provider, workspaceIndex, readonlyModel
|
|
251
|
+
const receiverExtras = emitReceiverBoundCalls(graph, indexes, parsedFiles, nodeLookup, handledSites, provider, workspaceIndex, readonlyModel, {
|
|
252
|
+
recordResolutionOutcome,
|
|
253
|
+
});
|
|
246
254
|
const unresolvedReceiverExtras = provider.emitUnresolvedReceiverEdges !== undefined
|
|
247
255
|
? provider.emitUnresolvedReceiverEdges(graph, indexes, parsedFiles, nodeLookup, handledSites, readonlyModel)
|
|
248
256
|
: 0;
|
|
@@ -253,6 +261,7 @@ export function runScopeResolution(input, provider) {
|
|
|
253
261
|
resolveAdlCandidates: provider.resolveAdlCandidates,
|
|
254
262
|
conversionRankFn: provider.conversionRankFn,
|
|
255
263
|
constraintCompatibility: provider.constraintCompatibility,
|
|
264
|
+
recordResolutionOutcome,
|
|
256
265
|
});
|
|
257
266
|
const { emitted, skipped } = emitReferencesViaLookup(graph, indexes, referenceIndex, nodeLookup, handledSites);
|
|
258
267
|
const importsEmitted = emitImportEdges(graph, indexes.imports, indexes.scopeTree, provider.importEdgeReason);
|
|
@@ -274,5 +283,6 @@ export function runScopeResolution(input, provider) {
|
|
|
274
283
|
resolve: resolveStats,
|
|
275
284
|
referenceEdgesEmitted: emitted + receiverExtras + unresolvedReceiverExtras + freeCallExtras,
|
|
276
285
|
referenceSkipped: skipped,
|
|
286
|
+
resolutionOutcomes,
|
|
277
287
|
};
|
|
278
288
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Range } from '../../../_shared/index.js';
|
|
2
|
+
export type ResolutionSuppressionReason = 'adl-ordinary-lookup-blocked' | 'conversion-rank-tied' | 'inline-ns-ambiguous' | 'overload-ambiguous' | 'overload-ambiguous-normalization';
|
|
3
|
+
export type ResolutionOutcome = {
|
|
4
|
+
readonly kind: 'resolved';
|
|
5
|
+
readonly targetId: string;
|
|
6
|
+
readonly phase: string;
|
|
7
|
+
readonly filePath: string;
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly range: Range;
|
|
10
|
+
} | {
|
|
11
|
+
readonly kind: 'suppressed';
|
|
12
|
+
readonly reason: ResolutionSuppressionReason;
|
|
13
|
+
/**
|
|
14
|
+
* Scope-resolution definition IDs considered by the suppression decision.
|
|
15
|
+
* For `inline-ns-ambiguous` this is currently empty because the
|
|
16
|
+
* qualified namespace resolver returns only an `ambiguous` sentinel.
|
|
17
|
+
*/
|
|
18
|
+
readonly candidateIds: readonly string[];
|
|
19
|
+
readonly phase: string;
|
|
20
|
+
readonly filePath: string;
|
|
21
|
+
readonly name: string;
|
|
22
|
+
readonly range: Range;
|
|
23
|
+
};
|
|
24
|
+
export type ResolutionOutcomeRecorder = (outcome: ResolutionOutcome) => void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/types/pipeline.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { KnowledgeGraph } from '../core/graph/types.js';
|
|
2
2
|
import { CommunityDetectionResult } from '../core/ingestion/community-processor.js';
|
|
3
3
|
import { ProcessDetectionResult } from '../core/ingestion/process-processor.js';
|
|
4
|
+
import type { ResolutionOutcome } from '../core/ingestion/scope-resolution/resolution-outcome.js';
|
|
4
5
|
export interface PipelineResult {
|
|
5
6
|
graph: KnowledgeGraph;
|
|
6
7
|
/** Absolute path to the repo root — used for lazy file reads during LadybugDB loading */
|
|
@@ -9,6 +10,12 @@ export interface PipelineResult {
|
|
|
9
10
|
totalFileCount: number;
|
|
10
11
|
communityResult?: CommunityDetectionResult;
|
|
11
12
|
processResult?: ProcessDetectionResult;
|
|
13
|
+
/**
|
|
14
|
+
* Additive diagnostics for registry-primary resolution decisions that
|
|
15
|
+
* deliberately suppress edge emission. Empty means no diagnostic was
|
|
16
|
+
* produced; graph edge semantics are unchanged.
|
|
17
|
+
*/
|
|
18
|
+
resolutionOutcomes: readonly ResolutionOutcome[];
|
|
12
19
|
/**
|
|
13
20
|
* True if the parse phase spawned a worker pool for this run. False means
|
|
14
21
|
* the sequential fallback handled every chunk. Primarily a test affordance
|
package/package.json
CHANGED