diff --git a/gitnexus/src/core/ingestion/pipeline.ts b/gitnexus/src/core/ingestion/pipeline.ts index ec5828b3b2..8506b42262 100644 --- a/gitnexus/src/core/ingestion/pipeline.ts +++ b/gitnexus/src/core/ingestion/pipeline.ts @@ -34,6 +34,7 @@ import { mroPhase, communitiesPhase, processesPhase, + type ScopeResolutionOutput, type PipelinePhase, type CommunitiesOutput, type ProcessesOutput, @@ -182,6 +183,10 @@ export const runPipelineFromRepo = async ( let communityResult: CommunitiesOutput['communityResult'] | undefined; let processResult: ProcessesOutput['processResult'] | undefined; + const resolutionOutcomes = getPhaseOutput( + results, + 'scopeResolution', + ).resolutionOutcomes; if (!options?.skipGraphPhases) { communityResult = getPhaseOutput(results, 'communities').communityResult; @@ -208,6 +213,7 @@ export const runPipelineFromRepo = async ( totalFileCount: totalFiles, communityResult, processResult, + resolutionOutcomes, usedWorkerPool, }; }; diff --git a/gitnexus/src/core/ingestion/scope-resolution/passes/free-call-fallback.ts b/gitnexus/src/core/ingestion/scope-resolution/passes/free-call-fallback.ts index 0d537db076..fcfb6ef327 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/passes/free-call-fallback.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/passes/free-call-fallback.ts @@ -30,6 +30,10 @@ import type { SemanticModel } from '../../model/semantic-model.js'; import type { WorkspaceResolutionIndex } from '../workspace-index.js'; import type { GraphNodeLookup } from '../graph-bridge/node-lookup.js'; import type { ScopeResolver } from '../contract/scope-resolver.js'; +import type { + ResolutionOutcomeRecorder, + ResolutionSuppressionReason, +} from '../resolution-outcome.js'; import { resolveCallerGraphId, resolveDefGraphId } from '../graph-bridge/ids.js'; import { findAllCallableBindingsInScope, @@ -79,6 +83,7 @@ export function emitFreeCallFallback( * fail at the call site. Three-valued; `'unknown'` keeps the * candidate (monotonicity). */ readonly constraintCompatibility?: ScopeResolver['constraintCompatibility']; + readonly recordResolutionOutcome?: ResolutionOutcomeRecorder; } = {}, ): number { let emitted = 0; @@ -152,9 +157,18 @@ export function emitFreeCallFallback( // Cross-file candidates are shadowing; keep first-match. const sameFile = narrowed.every((d) => d.filePath === narrowed[0]!.filePath); if (sameFile) { - handledSites.add( - `${parsed.filePath}:${site.atRange.startLine}:${site.atRange.startCol}`, - ); + recordSuppressedOutcome(options.recordResolutionOutcome, { + phase: 'free-call-fallback', + filePath: parsed.filePath, + name: site.name, + range: site.atRange, + reason: suppressionReasonForOverload(narrowed, site.arity, { + conversionRankFn: options.conversionRankFn, + argumentTypes: site.argumentTypes, + }), + candidates: narrowed, + }); + handledSites.add(siteKey(parsed.filePath, site)); continue; } } @@ -186,7 +200,19 @@ export function emitFreeCallFallback( parsedFiles, ); - const siteKey = `${parsed.filePath}:${site.atRange.startLine}:${site.atRange.startCol}`; + const key = siteKey(parsed.filePath, site); + if (adlSuppressed && ordinary.length === 0) { + recordSuppressedOutcome(options.recordResolutionOutcome, { + phase: 'free-call-fallback', + filePath: parsed.filePath, + name: site.name, + range: site.atRange, + reason: 'adl-ordinary-lookup-blocked', + candidates: ordinary, + }); + handledSites.add(key); + continue; + } if (adl === undefined || adl.length === 0) { // No ADL contribution. Default behavior: `ordinary[0]` — // scope-chain walk preserves local-shadows-import precedence. @@ -210,7 +236,7 @@ export function emitFreeCallFallback( if (narrowed.length === 1) { fnDef = narrowed[0]; } else if (narrowed.length === 0) { - handledSites.add(siteKey); + handledSites.add(key); continue; } else { // >1 survivors: same-file → suppress (true overloads, @@ -219,7 +245,18 @@ export function emitFreeCallFallback( // first-match (shadowing semantics). const sameFile = narrowed.every((d) => d.filePath === narrowed[0]!.filePath); if (sameFile) { - handledSites.add(siteKey); + recordSuppressedOutcome(options.recordResolutionOutcome, { + phase: 'free-call-fallback', + filePath: parsed.filePath, + name: site.name, + range: site.atRange, + reason: suppressionReasonForOverload(narrowed, site.arity, { + conversionRankFn: options.conversionRankFn, + argumentTypes: site.argumentTypes, + }), + candidates: narrowed, + }); + handledSites.add(key); continue; } fnDef = ordinary[0]; @@ -246,16 +283,27 @@ export function emitFreeCallFallback( if (narrowed.length === 1) { fnDef = narrowed[0]; } else if (narrowed.length === 0) { - handledSites.add(siteKey); + handledSites.add(key); continue; } else if (narrowed.length > 1) { + recordSuppressedOutcome(options.recordResolutionOutcome, { + phase: 'free-call-fallback', + filePath: parsed.filePath, + name: site.name, + range: site.atRange, + reason: suppressionReasonForOverload(narrowed, site.arity, { + conversionRankFn: options.conversionRankFn, + argumentTypes: site.argumentTypes, + }), + candidates: narrowed, + }); if (isOverloadAmbiguousAfterNormalization(narrowed, site.arity)) { - handledSites.add(siteKey); + handledSites.add(key); continue; } // Multiple survivors remain after conversion-rank scoring; // suppress instead of picking arbitrarily. - handledSites.add(siteKey); + handledSites.add(key); continue; } } @@ -295,7 +343,7 @@ export function emitFreeCallFallback( // Always mark the site as handled — even when the dedup-collapse // means we don't add a new edge — so `emit-references` skips its // potentially-wrong fallback for the same site. - handledSites.add(`${parsed.filePath}:${site.atRange.startLine}:${site.atRange.startCol}`); + handledSites.add(siteKey(parsed.filePath, site)); const relId = `rel:CALLS:${callerGraphId}->${tgtGraphId}`; if (seen.has(relId)) continue; seen.add(relId); @@ -315,6 +363,61 @@ export function emitFreeCallFallback( return emitted; } +function siteKey( + filePath: string, + site: { readonly atRange: { readonly startLine: number; readonly startCol: number } }, +): string { + return `${filePath}:${site.atRange.startLine}:${site.atRange.startCol}`; +} + +function suppressionReasonForOverload( + candidates: readonly SymbolDefinition[], + arity: number | undefined, + ctx: { + readonly conversionRankFn?: ConversionRankFn; + readonly argumentTypes?: readonly string[]; + }, +): ResolutionSuppressionReason { + if (isOverloadAmbiguousAfterNormalization(candidates, arity)) { + return 'overload-ambiguous-normalization'; + } + if ( + ctx.conversionRankFn !== undefined && + ctx.argumentTypes !== undefined && + ctx.argumentTypes.length > 0 + ) { + return 'conversion-rank-tied'; + } + return 'overload-ambiguous'; +} + +function recordSuppressedOutcome( + record: ResolutionOutcomeRecorder | undefined, + input: { + readonly phase: string; + readonly filePath: string; + readonly name: string; + readonly range: { + readonly startLine: number; + readonly startCol: number; + readonly endLine: number; + readonly endCol: number; + }; + readonly reason: ResolutionSuppressionReason; + readonly candidates: readonly SymbolDefinition[]; + }, +): void { + record?.({ + kind: 'suppressed', + phase: input.phase, + filePath: input.filePath, + name: input.name, + range: input.range, + reason: input.reason, + candidateIds: input.candidates.map((d) => d.nodeId), + }); +} + /** * Build a `simpleName -> callable defs` index from `scopes.defs` once per * pass. Mirrors the filter the old per-site scan applied: Function / diff --git a/gitnexus/src/core/ingestion/scope-resolution/passes/receiver-bound-calls.ts b/gitnexus/src/core/ingestion/scope-resolution/passes/receiver-bound-calls.ts index ac7164c70f..59b6c323dc 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/passes/receiver-bound-calls.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/passes/receiver-bound-calls.ts @@ -65,6 +65,10 @@ import { extractTemplateArguments, stripTemplateArguments, } from '../../utils/template-arguments.js'; +import type { + ResolutionOutcomeRecorder, + ResolutionSuppressionReason, +} from '../resolution-outcome.js'; /** Subset of `ScopeResolver` consumed by this pass. Accepting the * subset rather than the full provider keeps tests and partial @@ -140,6 +144,9 @@ export function emitReceiverBoundCalls( provider: ReceiverBoundProviderSubset, index: WorkspaceResolutionIndex, model: SemanticModel, + options: { + readonly recordResolutionOutcome?: ResolutionOutcomeRecorder; + } = {}, ): number { let emitted = 0; // Per-pass dedup so the multiple cases don't double-emit if two of @@ -498,6 +505,15 @@ export function emitReceiverBoundCalls( if (memberDef === 'ambiguous') { // Same-name ambiguity across inline-namespace children (#1564): // suppress edge emission, mark site handled. + options.recordResolutionOutcome?.({ + kind: 'suppressed', + phase: 'receiver-bound-calls', + filePath: parsed.filePath, + name: site.name, + range: site.atRange, + reason: 'inline-ns-ambiguous', + candidateIds: [], + }); handledSites.add(siteKey); continue; } @@ -702,6 +718,7 @@ export function emitReceiverBoundCalls( const chain = [ownerDef.nodeId, ...scopes.methodDispatch.mroFor(ownerDef.nodeId)]; let memberDef: SymbolDefinition | undefined; let ambiguous = false; + let ambiguousOwnerId: string | undefined; // Track whether the chain walk filtered out any static-only // candidates. When it did and the chain ended with no // legitimate instance member, we mark the site as handled so @@ -733,6 +750,7 @@ export function emitReceiverBoundCalls( const picked = pickFirstNonStaticOnly(ownerId, memberName, site, model, provider); if (picked === OVERLOAD_AMBIGUOUS) { ambiguous = true; + ambiguousOwnerId = ownerId; break; } if (picked === STATIC_ONLY_FILTERED) { @@ -754,6 +772,15 @@ export function emitReceiverBoundCalls( // Suppress and mark handled so `emitReferencesViaLookup` // doesn't re-emit the pre-resolved reference. See // OVERLOAD_AMBIGUOUS docstring for the upstream cause. + recordReceiverOverloadSuppression( + options.recordResolutionOutcome, + parsed.filePath, + site, + ambiguousOwnerId ?? ownerDef.nodeId, + memberName, + model, + provider, + ); handledSites.add(siteKey); continue; } @@ -830,6 +857,15 @@ export function emitReceiverBoundCalls( resolveDefGraphId(valueDef.filePath, valueDef, nodeLookup) ?? valueDef.nodeId; const picked = pickOverload(ownerGraphId, memberName, site, model, provider); if (picked === OVERLOAD_AMBIGUOUS) { + recordReceiverOverloadSuppression( + options.recordResolutionOutcome, + parsed.filePath, + site, + ownerGraphId, + memberName, + model, + provider, + ); handledSites.add(siteKey); continue; } @@ -1017,3 +1053,49 @@ function pickFirstNonStaticOnly( if (candidates.length > 1) return OVERLOAD_AMBIGUOUS; return candidates[0] ?? overloads[0]; } + +function recordReceiverOverloadSuppression( + record: ResolutionOutcomeRecorder | undefined, + filePath: string, + site: ParsedFile['referenceSites'][number], + ownerId: string, + memberName: string, + model: SemanticModel, + provider: ReceiverBoundProviderSubset, +): void { + if (record === undefined) return; + const overloads = model.methods.lookupAllByOwner(ownerId, memberName); + const candidates = narrowOverloadCandidates(overloads, site.arity, site.argumentTypes, { + argumentTypeClasses: site.argumentTypeClasses, + conversionRankFn: provider.conversionRankFn, + constraintCompatibility: provider.constraintCompatibility, + }); + const reason: ResolutionSuppressionReason = isOverloadAmbiguousAfterNormalization( + candidates, + site.arity, + ) + ? 'overload-ambiguous-normalization' + : hasConversionRankingSignal(site, provider) + ? 'conversion-rank-tied' + : 'overload-ambiguous'; + record({ + kind: 'suppressed', + phase: 'receiver-bound-calls', + filePath, + name: site.name, + range: site.atRange, + reason, + candidateIds: candidates.map((d) => d.nodeId), + }); +} + +function hasConversionRankingSignal( + site: ParsedFile['referenceSites'][number], + provider: ReceiverBoundProviderSubset, +): boolean { + return ( + provider.conversionRankFn !== undefined && + site.argumentTypes !== undefined && + site.argumentTypes.length > 0 + ); +} diff --git a/gitnexus/src/core/ingestion/scope-resolution/pipeline/phase.ts b/gitnexus/src/core/ingestion/scope-resolution/pipeline/phase.ts index 98a9f8994a..31b712d78e 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/pipeline/phase.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/pipeline/phase.ts @@ -37,6 +37,7 @@ import { readFileContents } from '../../filesystem-walker.js'; import { runScopeResolution } from './run.js'; import { SCOPE_RESOLVERS } from './registry.js'; import { isDev, isSemanticModelValidatorEnabled } from '../../utils/env.js'; +import type { ResolutionOutcome } from '../resolution-outcome.js'; import { logger } from '../../../logger.js'; export interface ScopeResolutionOutput { @@ -48,6 +49,8 @@ export interface ScopeResolutionOutput { readonly importsEmitted: number; /** Reference (CALLS / ACCESSES / INHERITS / USES) edges emitted. */ readonly referenceEdgesEmitted: number; + /** Additive stream of resolver diagnostics; does not affect graph edges. */ + readonly resolutionOutcomes: readonly ResolutionOutcome[]; /** Per-language breakdown for telemetry / shadow-parity. */ readonly perLanguage: ReadonlyMap< SupportedLanguages, @@ -64,6 +67,7 @@ const NOOP_OUTPUT: ScopeResolutionOutput = Object.freeze({ filesProcessed: 0, importsEmitted: 0, referenceEdgesEmitted: 0, + resolutionOutcomes: [], perLanguage: new Map(), }); @@ -116,6 +120,7 @@ export const scopeResolutionPhase: PipelinePhase = { let totalImports = 0; let totalRefs = 0; let anyRan = false; + const resolutionOutcomes: ResolutionOutcome[] = []; const perLanguage = new Map< SupportedLanguages, { @@ -156,6 +161,9 @@ export const scopeResolutionPhase: PipelinePhase = { treeCache: scopeTreeCache, resolutionConfig, preExtractedParsedFiles: preExtractedByPath, + recordResolutionOutcome: (outcome) => { + resolutionOutcomes.push(outcome); + }, onWarn: (msg) => { if (isSemanticModelValidatorEnabled()) { logger.warn(`[scope-resolution:${lang}] ${msg}`); @@ -197,6 +205,7 @@ export const scopeResolutionPhase: PipelinePhase = { filesProcessed: totalFiles, importsEmitted: totalImports, referenceEdgesEmitted: totalRefs, + resolutionOutcomes, perLanguage, }; }, diff --git a/gitnexus/src/core/ingestion/scope-resolution/pipeline/run.ts b/gitnexus/src/core/ingestion/scope-resolution/pipeline/run.ts index ae6ec5a23b..777cdb6394 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/pipeline/run.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/pipeline/run.ts @@ -44,6 +44,7 @@ import { emitImportEdges } from '../graph-bridge/imports-to-edges.js'; import type { ScopeResolver } from '../contract/scope-resolver.js'; import { findClassBindingInScope, findEnclosingClassDef } from '../scope/walkers.js'; import { buildWorkspaceResolutionIndex } from '../workspace-index.js'; +import type { ResolutionOutcome, ResolutionOutcomeRecorder } from '../resolution-outcome.js'; import { logger } from '../../../logger.js'; @@ -161,6 +162,11 @@ interface RunScopeResolutionInput { * Cache miss is safe — falls back to fresh extract. */ readonly preExtractedParsedFiles?: ReadonlyMap; + /** + * Optional additive diagnostics sink. Resolver passes call this when they + * intentionally suppress an edge; the graph remains unchanged. + */ + readonly recordResolutionOutcome?: ResolutionOutcomeRecorder; } interface RunScopeResolutionStats { @@ -170,6 +176,7 @@ interface RunScopeResolutionStats { readonly resolve: ResolveStats; readonly referenceEdgesEmitted: number; readonly referenceSkipped: number; + readonly resolutionOutcomes: readonly ResolutionOutcome[]; } export function runScopeResolution( @@ -178,6 +185,11 @@ export function runScopeResolution( ): RunScopeResolutionStats { const { graph, files } = input; const onWarn = input.onWarn ?? (() => {}); + const resolutionOutcomes: ResolutionOutcome[] = []; + const recordResolutionOutcome: ResolutionOutcomeRecorder = (outcome) => { + resolutionOutcomes.push(outcome); + input.recordResolutionOutcome?.(outcome); + }; const PROF = process.env.PROF_SCOPE_RESOLUTION === '1'; const tStart = PROF ? process.hrtime.bigint() : 0n; let fileContents: Map | undefined; @@ -248,6 +260,7 @@ export function runScopeResolution( resolve: { sitesProcessed: 0, referencesEmitted: 0, unresolved: 0 }, referenceEdgesEmitted: 0, referenceSkipped: 0, + resolutionOutcomes, }; } @@ -359,6 +372,9 @@ export function runScopeResolution( provider, workspaceIndex, readonlyModel, + { + recordResolutionOutcome, + }, ); const unresolvedReceiverExtras = provider.emitUnresolvedReceiverEdges !== undefined @@ -387,6 +403,7 @@ export function runScopeResolution( resolveAdlCandidates: provider.resolveAdlCandidates, conversionRankFn: provider.conversionRankFn, constraintCompatibility: provider.constraintCompatibility, + recordResolutionOutcome, }, ); const { emitted, skipped } = emitReferencesViaLookup( @@ -424,5 +441,6 @@ export function runScopeResolution( resolve: resolveStats, referenceEdgesEmitted: emitted + receiverExtras + unresolvedReceiverExtras + freeCallExtras, referenceSkipped: skipped, + resolutionOutcomes, }; } diff --git a/gitnexus/src/core/ingestion/scope-resolution/resolution-outcome.ts b/gitnexus/src/core/ingestion/scope-resolution/resolution-outcome.ts new file mode 100644 index 0000000000..4eabfa29bb --- /dev/null +++ b/gitnexus/src/core/ingestion/scope-resolution/resolution-outcome.ts @@ -0,0 +1,34 @@ +import type { Range } from 'gitnexus-shared'; + +export type ResolutionSuppressionReason = + | 'adl-ordinary-lookup-blocked' + | 'conversion-rank-tied' + | 'inline-ns-ambiguous' + | 'overload-ambiguous' + | 'overload-ambiguous-normalization'; + +export type ResolutionOutcome = + | { + readonly kind: 'resolved'; + readonly targetId: string; + readonly phase: string; + readonly filePath: string; + readonly name: string; + readonly range: Range; + } + | { + readonly kind: 'suppressed'; + readonly reason: ResolutionSuppressionReason; + /** + * Scope-resolution definition IDs considered by the suppression decision. + * For `inline-ns-ambiguous` this is currently empty because the + * qualified namespace resolver returns only an `ambiguous` sentinel. + */ + readonly candidateIds: readonly string[]; + readonly phase: string; + readonly filePath: string; + readonly name: string; + readonly range: Range; + }; + +export type ResolutionOutcomeRecorder = (outcome: ResolutionOutcome) => void; diff --git a/gitnexus/src/types/pipeline.ts b/gitnexus/src/types/pipeline.ts index 331bdd6fcf..becf817577 100644 --- a/gitnexus/src/types/pipeline.ts +++ b/gitnexus/src/types/pipeline.ts @@ -1,6 +1,7 @@ import type { KnowledgeGraph } from '../core/graph/types.js'; import { CommunityDetectionResult } from '../core/ingestion/community-processor.js'; import { ProcessDetectionResult } from '../core/ingestion/process-processor.js'; +import type { ResolutionOutcome } from '../core/ingestion/scope-resolution/resolution-outcome.js'; // CLI-specific: in-memory result with graph + detection results export interface PipelineResult { @@ -11,6 +12,12 @@ export interface PipelineResult { totalFileCount: number; communityResult?: CommunityDetectionResult; processResult?: ProcessDetectionResult; + /** + * Additive diagnostics for registry-primary resolution decisions that + * deliberately suppress edge emission. Empty means no diagnostic was + * produced; graph edge semantics are unchanged. + */ + resolutionOutcomes: readonly ResolutionOutcome[]; /** * True if the parse phase spawned a worker pool for this run. False means * the sequential fallback handled every chunk. Primarily a test affordance diff --git a/gitnexus/test/integration/resolvers/cpp.test.ts b/gitnexus/test/integration/resolvers/cpp.test.ts index 90bc751171..e33fa66329 100644 --- a/gitnexus/test/integration/resolvers/cpp.test.ts +++ b/gitnexus/test/integration/resolvers/cpp.test.ts @@ -9,6 +9,7 @@ import { getRelationships, getNodesByLabel, getNodesByLabelFull, + getResolutionOutcomes, edgeSet, runPipelineFromRepo, createResolverParityIt, @@ -1819,6 +1820,21 @@ describe('C++ ambiguous integer-width overloads', () => { // GitNexus does not have. The resolver must suppress entirely. expect(processCalls.length).toBe(0); }); + + it('records a structured suppression reason for normalization ambiguity', () => { + const outcomes = getResolutionOutcomes(result).filter( + (o) => + o.kind === 'suppressed' && + o.name === 'process' && + o.phase === 'receiver-bound-calls' && + o.filePath.endsWith('caller.cpp') && + o.reason === 'overload-ambiguous-normalization', + ); + + expect(outcomes.length).toBeGreaterThan(0); + expect(outcomes[0]?.candidateIds.length).toBe(2); + expect(outcomes[0]?.range.startLine).toBeGreaterThan(0); + }); }); // --------------------------------------------------------------------------- @@ -1893,6 +1909,20 @@ describe('C++ overload resolution — conversion-rank disambiguation (#1578)', ( // Contract: zero edges for ALL h() call sites combined (dedup). expect(hCalls.length).toBe(0); }); + + it('records a structured suppression reason for conversion-rank ties', () => { + const outcomes = getResolutionOutcomes(result).filter( + (o) => + o.kind === 'suppressed' && + o.name === 'h' && + o.phase === 'free-call-fallback' && + o.reason === 'conversion-rank-tied', + ); + + expect(outcomes.length).toBeGreaterThan(0); + expect(outcomes[0]?.candidateIds.length).toBe(2); + expect(outcomes[0]?.range.startLine).toBeGreaterThan(0); + }); }); // C++ overload resolution: pointer/nullptr/ellipsis conversion ranks (#1637) @@ -2722,6 +2752,20 @@ describe('C++ ADL — non-function ordinary lookup suppresses ADL', () => { // `e` is audit::Event, audit::record should NOT be discovered. expect(recordCalls.length).toBe(0); }); + + it('records a structured suppression reason for ADL blocker lookup', () => { + const outcomes = getResolutionOutcomes(result).filter( + (o) => + o.kind === 'suppressed' && + o.name === 'record' && + o.phase === 'free-call-fallback' && + o.reason === 'adl-ordinary-lookup-blocked', + ); + + expect(outcomes.length).toBeGreaterThan(0); + expect(outcomes[0]?.candidateIds.length).toBe(0); + expect(outcomes[0]?.range.startLine).toBeGreaterThan(0); + }); }); describe('C++ ADL — inner callable + outer non-callable: ADL not suppressed', () => { @@ -2984,6 +3028,20 @@ describe('C++ inline namespace — ambiguous same-name across inline children (# // the same name. The resolver must suppress rather than pick arbitrarily. expect(fooCalls.length).toBe(0); }); + + it('records a structured suppression reason for inline namespace ambiguity', () => { + const outcomes = getResolutionOutcomes(result).filter( + (o) => + o.kind === 'suppressed' && + o.name === 'foo' && + o.phase === 'receiver-bound-calls' && + o.reason === 'inline-ns-ambiguous', + ); + + expect(outcomes.length).toBeGreaterThan(0); + expect(outcomes[0]?.candidateIds.length).toBe(0); + expect(outcomes[0]?.range.startLine).toBeGreaterThan(0); + }); }); describe('C++ inline namespace — ambiguous distinct signatures (conservative suppress)', () => { diff --git a/gitnexus/test/integration/resolvers/helpers.ts b/gitnexus/test/integration/resolvers/helpers.ts index 74152c6ea7..f599835b58 100644 --- a/gitnexus/test/integration/resolvers/helpers.ts +++ b/gitnexus/test/integration/resolvers/helpers.ts @@ -218,6 +218,7 @@ const LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES: Readonly {