gitnexus 1.6.5 → 1.6.6-rc.2

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 (42) hide show
  1. package/dist/_shared/index.d.ts +2 -2
  2. package/dist/_shared/index.d.ts.map +1 -1
  3. package/dist/_shared/index.js.map +1 -1
  4. package/dist/_shared/scope-resolution/registries/context.d.ts +27 -0
  5. package/dist/_shared/scope-resolution/registries/context.d.ts.map +1 -1
  6. package/dist/_shared/scope-resolution/registries/context.js.map +1 -1
  7. package/dist/_shared/scope-resolution/symbol-definition.d.ts +20 -0
  8. package/dist/_shared/scope-resolution/symbol-definition.d.ts.map +1 -1
  9. package/dist/core/ingestion/language-provider.d.ts +29 -0
  10. package/dist/core/ingestion/languages/c-cpp.js +46 -0
  11. package/dist/core/ingestion/languages/cpp/arity-metadata.d.ts +17 -0
  12. package/dist/core/ingestion/languages/cpp/arity-metadata.js +51 -2
  13. package/dist/core/ingestion/languages/cpp/arity.d.ts +4 -1
  14. package/dist/core/ingestion/languages/cpp/arity.js +4 -1
  15. package/dist/core/ingestion/languages/cpp/captures.js +73 -0
  16. package/dist/core/ingestion/languages/cpp/constraint-extractor.d.ts +73 -0
  17. package/dist/core/ingestion/languages/cpp/constraint-extractor.js +308 -0
  18. package/dist/core/ingestion/languages/cpp/constraint-filter.d.ts +31 -0
  19. package/dist/core/ingestion/languages/cpp/constraint-filter.js +135 -0
  20. package/dist/core/ingestion/languages/cpp/scope-resolver.js +6 -0
  21. package/dist/core/ingestion/languages/cpp/type-classifier.d.ts +26 -0
  22. package/dist/core/ingestion/languages/cpp/type-classifier.js +49 -0
  23. package/dist/core/ingestion/model/symbol-table.d.ts +2 -1
  24. package/dist/core/ingestion/model/symbol-table.js +3 -0
  25. package/dist/core/ingestion/parsing-processor.js +35 -2
  26. package/dist/core/ingestion/scope-extractor.js +63 -0
  27. package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +21 -1
  28. package/dist/core/ingestion/scope-resolution/graph-bridge/ids.d.ts +1 -0
  29. package/dist/core/ingestion/scope-resolution/graph-bridge/ids.js +14 -0
  30. package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.js +12 -0
  31. package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.d.ts +11 -1
  32. package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +50 -21
  33. package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.d.ts +30 -7
  34. package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.js +49 -18
  35. package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.d.ts +1 -1
  36. package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +10 -4
  37. package/dist/core/ingestion/scope-resolution/pipeline/run.js +1 -0
  38. package/dist/core/ingestion/utils/template-arguments.d.ts +19 -0
  39. package/dist/core/ingestion/utils/template-arguments.js +30 -0
  40. package/dist/core/ingestion/workers/parse-worker.d.ts +2 -1
  41. package/dist/core/ingestion/workers/parse-worker.js +1 -0
  42. package/package.json +1 -1
@@ -11,7 +11,7 @@ import { getDefinitionNodeFromCaptures, findEnclosingClassInfo, getLabelFromCapt
11
11
  import { detectFrameworkFromAST } from './framework-detection.js';
12
12
  import { buildTypeEnv } from './type-env.js';
13
13
  import { buildMethodProps, arityForIdFromInfo, typeTagForId, constTagForId, buildCollisionGroups, } from './utils/method-props.js';
14
- import { extractTemplateArguments, templateArgumentsIdTag } from './utils/template-arguments.js';
14
+ import { extractTemplateArguments, templateArgumentsIdTag, templateConstraintsIdTag, } from './utils/template-arguments.js';
15
15
  import { logger } from '../logger.js';
16
16
  import { getTreeSitterBufferSize, getTreeSitterContentByteLength, TREE_SITTER_MAX_BUFFER, } from './constants.js';
17
17
  // ============================================================================
@@ -57,6 +57,7 @@ export const mergeChunkResults = (graph, symbolTable, chunkResults) => {
57
57
  parameterCount: sym.parameterCount,
58
58
  requiredParameterCount: sym.requiredParameterCount,
59
59
  parameterTypes: sym.parameterTypes,
60
+ parameterTypeClasses: sym.parameterTypeClasses,
60
61
  returnType: sym.returnType,
61
62
  declaredType: sym.declaredType,
62
63
  templateArguments: sym.templateArguments,
@@ -499,7 +500,35 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, sco
499
500
  classTemplateArguments.length > 0
500
501
  ? templateArgumentsIdTag(classTemplateArguments)
501
502
  : '';
502
- const nodeId = generateId(nodeLabel, `${file.path}:${qualifiedName}${classTemplateTag}${arityTag}`);
503
+ // SFINAE / `requires`-clause aware ID disambiguation (issue #1579).
504
+ // Function-template overloads with identical parameterTypes but
505
+ // mutually-exclusive constraints (e.g. `enable_if_t<is_integral_v<T>>`
506
+ // vs `enable_if_t<is_floating_point_v<T>>`) need distinct graph
507
+ // nodes so the constraint-filter step in `narrowOverloadCandidates`
508
+ // has two candidates to narrow between. Without this tag they
509
+ // collapse to a single Function node and the SFINAE call resolves
510
+ // to only one edge regardless of which overload's constraint holds.
511
+ // The provider hook is the right invocation point — parsing-processor
512
+ // sees raw tree-sitter matches without the `@`-prefixed synthetic
513
+ // captures `scope-extractor` consumes, so we delegate extraction to
514
+ // the language adapter (C++ implements this; other languages opt out).
515
+ let parsedTemplateConstraints = undefined;
516
+ let constraintsTag = '';
517
+ if ((nodeLabel === 'Function' || nodeLabel === 'Method') &&
518
+ provider.extractTemplateConstraints !== undefined &&
519
+ definitionNode !== null) {
520
+ try {
521
+ parsedTemplateConstraints = provider.extractTemplateConstraints(definitionNode);
522
+ if (parsedTemplateConstraints !== undefined) {
523
+ constraintsTag = templateConstraintsIdTag(parsedTemplateConstraints);
524
+ }
525
+ }
526
+ catch {
527
+ parsedTemplateConstraints = undefined;
528
+ constraintsTag = '';
529
+ }
530
+ }
531
+ const nodeId = generateId(nodeLabel, `${file.path}:${qualifiedName}${classTemplateTag}${arityTag}${constraintsTag}`);
503
532
  const classNodeForSymbol = definitionNodeForRange || definitionNode || nameNode;
504
533
  const qualifiedTypeName = extractedClassSymbol?.qualifiedName ??
505
534
  (classNodeForSymbol && provider.classExtractor?.isTypeDeclaration(classNodeForSymbol)
@@ -528,6 +557,9 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, sco
528
557
  ...(classTemplateArguments !== undefined && classTemplateArguments.length > 0
529
558
  ? { templateArguments: classTemplateArguments }
530
559
  : {}),
560
+ ...(parsedTemplateConstraints !== undefined
561
+ ? { templateConstraints: parsedTemplateConstraints }
562
+ : {}),
531
563
  ...(frameworkHint
532
564
  ? {
533
565
  astFrameworkMultiplier: frameworkHint.entryPointMultiplier,
@@ -579,6 +611,7 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, sco
579
611
  parameterCount: methodProps.parameterCount,
580
612
  requiredParameterCount: methodProps.requiredParameterCount,
581
613
  parameterTypes: methodProps.parameterTypes,
614
+ parameterTypeClasses: methodProps.parameterTypeClasses,
582
615
  returnType: methodProps.returnType,
583
616
  declaredType,
584
617
  templateArguments: classTemplateArguments,
@@ -372,8 +372,10 @@ function buildDefFromDeclarationMatch(match, anchor, filePath) {
372
372
  const parameterCount = parseIntCapture(match['@declaration.parameter-count']);
373
373
  const requiredParameterCount = parseIntCapture(match['@declaration.required-parameter-count']);
374
374
  const parameterTypes = parseJsonStringArrayCapture(match['@declaration.parameter-types']);
375
+ const parameterTypeClasses = parseJsonParameterTypeClassesCapture(match['@declaration.parameter-type-classes']);
375
376
  const declaredType = match['@declaration.field-type']?.text;
376
377
  const returnType = match['@declaration.return-type']?.text;
378
+ const templateConstraints = parseJsonCapture(match['@declaration.template-constraints']);
377
379
  return {
378
380
  nodeId: makeDefId(filePath, anchor.range, type, nameCap.text),
379
381
  filePath,
@@ -382,17 +384,77 @@ function buildDefFromDeclarationMatch(match, anchor, filePath) {
382
384
  ...(parameterCount !== undefined ? { parameterCount } : {}),
383
385
  ...(requiredParameterCount !== undefined ? { requiredParameterCount } : {}),
384
386
  ...(parameterTypes !== undefined ? { parameterTypes } : {}),
387
+ ...(parameterTypeClasses !== undefined ? { parameterTypeClasses } : {}),
385
388
  ...(declaredType !== undefined ? { declaredType } : {}),
386
389
  ...(returnType !== undefined ? { returnType } : {}),
387
390
  ...(templateArguments !== undefined ? { templateArguments } : {}),
391
+ ...(templateConstraints !== undefined ? { templateConstraints } : {}),
388
392
  };
389
393
  }
394
+ /** Parse an opaque JSON payload synthesized by per-language captures
395
+ * (e.g. C++ `@declaration.template-constraints`). Producer owns the
396
+ * shape; shared code threads it through as `unknown` per the
397
+ * `SymbolDefinition.templateConstraints` contract. */
398
+ function parseJsonCapture(cap) {
399
+ if (cap === undefined)
400
+ return undefined;
401
+ try {
402
+ return JSON.parse(cap.text);
403
+ }
404
+ catch {
405
+ return undefined;
406
+ }
407
+ }
390
408
  function parseIntCapture(cap) {
391
409
  if (cap === undefined)
392
410
  return undefined;
393
411
  const n = Number.parseInt(cap.text, 10);
394
412
  return Number.isFinite(n) ? n : undefined;
395
413
  }
414
+ function parseJsonParameterTypeClassesCapture(cap) {
415
+ if (cap === undefined)
416
+ return undefined;
417
+ try {
418
+ const parsed = JSON.parse(cap.text);
419
+ if (!Array.isArray(parsed))
420
+ return undefined;
421
+ const out = [];
422
+ for (const item of parsed) {
423
+ if (item === null || typeof item !== 'object')
424
+ return undefined;
425
+ const o = item;
426
+ if (typeof o.base !== 'string')
427
+ return undefined;
428
+ if (o.cv !== 'none' &&
429
+ o.cv !== 'const' &&
430
+ o.cv !== 'volatile' &&
431
+ o.cv !== 'const volatile' &&
432
+ o.cv !== 'unknown') {
433
+ return undefined;
434
+ }
435
+ if (o.indirection !== 'value' &&
436
+ o.indirection !== 'lvalue-ref' &&
437
+ o.indirection !== 'rvalue-ref' &&
438
+ o.indirection !== 'pointer' &&
439
+ o.indirection !== 'unknown') {
440
+ return undefined;
441
+ }
442
+ if (typeof o.pointerDepth !== 'number' || !Number.isFinite(o.pointerDepth)) {
443
+ return undefined;
444
+ }
445
+ out.push({
446
+ base: o.base,
447
+ cv: o.cv,
448
+ indirection: o.indirection,
449
+ pointerDepth: o.pointerDepth,
450
+ });
451
+ }
452
+ return out;
453
+ }
454
+ catch {
455
+ return undefined;
456
+ }
457
+ }
396
458
  function parseJsonStringArrayCapture(cap) {
397
459
  if (cap === undefined)
398
460
  return undefined;
@@ -747,6 +809,7 @@ const KNOWN_SUB_TAGS = new Set([
747
809
  '@declaration.parameter-count',
748
810
  '@declaration.required-parameter-count',
749
811
  '@declaration.parameter-types',
812
+ '@declaration.template-constraints',
750
813
  ]);
751
814
  /**
752
815
  * Return the anchor capture for a match — the one whose name begins with
@@ -250,7 +250,7 @@
250
250
  * Plan that introduced most of these invariants:
251
251
  * `docs/plans/2026-04-20-001-refactor-emit-pipeline-generalization-plan.md`.
252
252
  */
253
- import type { BindingRef, Callsite, ParsedFile, ScopeId, SupportedLanguages, SymbolDefinition } from '../../../../_shared/index.js';
253
+ import type { BindingRef, Callsite, ConstraintContext, ParsedFile, ScopeId, SupportedLanguages, SymbolDefinition } from '../../../../_shared/index.js';
254
254
  import type { KnowledgeGraph } from '../../../graph/types.js';
255
255
  import type { GraphNodeLookup } from '../graph-bridge/node-lookup.js';
256
256
  import { LanguageProvider } from '../../language-provider.js';
@@ -264,6 +264,9 @@ import type { ConversionRankFn } from '../passes/overload-narrowing.js';
264
264
  export type LinearizeStrategy = (classDefId: string, directParents: readonly string[], parentsByDefId: ReadonlyMap<string, readonly string[]>) => string[];
265
265
  /** Result of `ScopeResolver.arityCompatibility` — mirrors `RegistryProviders.arityCompatibility`. */
266
266
  export type ArityVerdict = 'compatible' | 'unknown' | 'incompatible';
267
+ /** Re-exported for ScopeResolver consumers — same shape as
268
+ * `RegistryProviders.constraintCompatibility`'s third parameter. */
269
+ export type { ConstraintContext } from '../../../../_shared/index.js';
267
270
  export interface ScopeResolver {
268
271
  /** Identity for telemetry + per-language flag check. */
269
272
  readonly language: SupportedLanguages;
@@ -337,6 +340,23 @@ export interface ScopeResolver {
337
340
  * `(def, callsite)` and need an adapter at the wiring site.
338
341
  */
339
342
  arityCompatibility(callsite: Callsite, def: SymbolDefinition): ArityVerdict;
343
+ /**
344
+ * Per-language constraint compatibility between a callsite and a
345
+ * candidate `def` that carries `templateConstraints` metadata.
346
+ * Mirrors `arityCompatibility` semantics: the three-valued verdict
347
+ * MUST treat `'unknown'` as keep-candidate (monotonicity — adding
348
+ * a predicate can only narrow correctly, never produce a wrong
349
+ * edge). Consulted by `narrowOverloadCandidates` after the arity
350
+ * and parameter-type filters.
351
+ *
352
+ * Optional. Languages without constrained-overload semantics
353
+ * (SFINAE, `requires` clauses, trait bounds, conditional types)
354
+ * leave this undefined and the constraint filter is a pass-through.
355
+ *
356
+ * C++ is the first consumer; see `languages/cpp/constraint-filter.ts`
357
+ * for the Tier-A predicate registry and Kleene 3-valued evaluator.
358
+ */
359
+ readonly constraintCompatibility?: (callsite: Callsite, def: SymbolDefinition, ctx: ConstraintContext) => ArityVerdict;
340
360
  /**
341
361
  * Compute the method-dispatch order for every Class def in the
342
362
  * workspace. Python uses depth-first first-seen via
@@ -41,6 +41,7 @@ export declare function resolveDefGraphId(filePath: string, def: {
41
41
  type?: NodeLabel;
42
42
  parameterTypes?: readonly string[];
43
43
  templateArguments?: readonly string[];
44
+ templateConstraints?: unknown;
44
45
  }, nodeLookup: GraphNodeLookup): string | undefined;
45
46
  /** Derive the simple (unqualified) name of a def from its `qualifiedName`. */
46
47
  export declare function simpleQualifiedName(def: SymbolDefinition): string | undefined;
@@ -18,6 +18,7 @@
18
18
  */
19
19
  import { generateId } from '../../../../lib/utils.js';
20
20
  import { qualifiedKey, simpleKey } from '../graph-bridge/node-lookup.js';
21
+ import { templateConstraintsIdTag } from '../../utils/template-arguments.js';
21
22
  /**
22
23
  * Labels that may legitimately ANCHOR a CALLS/ACCESSES edge as the
23
24
  * source ("caller"). A Variable / Property can be the TARGET of an
@@ -68,6 +69,19 @@ export function resolveDefGraphId(filePath, def, nodeLookup) {
68
69
  if (qn === undefined || qn.length === 0)
69
70
  return undefined;
70
71
  if (def.type !== undefined) {
72
+ // SFINAE / `requires`-clause disambiguation (issue #1579) — try the
73
+ // constraint-fingerprinted key FIRST. Two function-template overloads
74
+ // with identical `parameterTypes` but mutually-exclusive SFINAE
75
+ // constraints route to their distinct graph nodes via this key.
76
+ // Must run before the parameter-types key because both overloads
77
+ // share the latter.
78
+ if ((def.type === 'Function' || def.type === 'Method') &&
79
+ def.templateConstraints !== undefined) {
80
+ const cKey = qualifiedKey(filePath, def.type, `${qn}${templateConstraintsIdTag(def.templateConstraints)}`);
81
+ const cHit = nodeLookup.get(cKey);
82
+ if (cHit !== undefined)
83
+ return cHit;
84
+ }
71
85
  // Overload disambiguation: when the def carries parameter types,
72
86
  // try the parameter-typed key first so same-name same-arity
73
87
  // overloads route to their distinct graph nodes.
@@ -17,6 +17,7 @@
17
17
  * `SymbolDefinition.nodeId` values into the legacy graph-node ID
18
18
  * format that downstream consumers (queries, edges, MCP) expect.
19
19
  */
20
+ import { templateConstraintsIdTag } from '../../utils/template-arguments.js';
20
21
  /**
21
22
  * Parse a qualified name out of a Function/Method node id.
22
23
  *
@@ -86,6 +87,17 @@ export function buildGraphNodeLookup(graph) {
86
87
  // Each overload is unique — set unconditionally.
87
88
  lookup.set(pKey, node.id);
88
89
  }
90
+ // SFINAE / `requires`-clause disambiguation (issue #1579) — register
91
+ // a constraint-fingerprinted key so resolveDefGraphId can locate the
92
+ // correct overload by hashing the def's `templateConstraints`. Mirrors
93
+ // the parameter-types key but keys on the opaque constraint payload
94
+ // instead, separating two `process<T>` overloads whose
95
+ // `parameterTypes=['T']` would otherwise collide.
96
+ const tConstraints = props.templateConstraints;
97
+ if (tConstraints !== undefined && (node.label === 'Function' || node.label === 'Method')) {
98
+ const cKey = qualifiedKey(props.filePath, node.label, `${qualified}${templateConstraintsIdTag(tConstraints)}`);
99
+ lookup.set(cKey, node.id);
100
+ }
89
101
  if ((node.label === 'Class' ||
90
102
  node.label === 'Struct' ||
91
103
  node.label === 'Interface' ||
@@ -22,6 +22,7 @@ import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexe
22
22
  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
+ import type { ScopeResolver } from '../contract/scope-resolver.js';
25
26
  import { type ConversionRankFn } from './overload-narrowing.js';
26
27
  export declare function emitFreeCallFallback(graph: KnowledgeGraph, scopes: ScopeResolutionIndexes, parsedFiles: readonly ParsedFile[], nodeLookup: GraphNodeLookup, _referenceIndex: {
27
28
  readonly bySourceScope: ReadonlyMap<ScopeId, readonly Reference[]>;
@@ -44,6 +45,12 @@ export declare function emitFreeCallFallback(graph: KnowledgeGraph, scopes: Scop
44
45
  };
45
46
  }, callerParsed: ParsedFile, scopes: ScopeResolutionIndexes, parsedFiles: readonly ParsedFile[]) => readonly SymbolDefinition[] | undefined;
46
47
  readonly conversionRankFn?: ConversionRankFn;
48
+ /** Optional per-language constraint hook threaded into
49
+ * `narrowOverloadCandidates`. Drops candidates whose template
50
+ * constraints (e.g. C++ `enable_if_t`, C++20 `requires`) provably
51
+ * fail at the call site. Three-valued; `'unknown'` keeps the
52
+ * candidate (monotonicity). */
53
+ readonly constraintCompatibility?: ScopeResolver['constraintCompatibility'];
47
54
  }): number;
48
55
  /** Walk up from the call-site scope to the enclosing class scope,
49
56
  * pick a method member by name with overload narrowing on arity +
@@ -63,4 +70,7 @@ export declare function pickImplicitThisOverload(site: {
63
70
  readonly name: string;
64
71
  readonly arity?: number;
65
72
  readonly argumentTypes?: readonly string[];
66
- }, scopes: ScopeResolutionIndexes, workspaceIndex: WorkspaceResolutionIndex, model: SemanticModel, conversionRankFn?: ConversionRankFn): SymbolDefinition | undefined;
73
+ }, scopes: ScopeResolutionIndexes, workspaceIndex: WorkspaceResolutionIndex, model: SemanticModel, hookCtx?: {
74
+ readonly conversionRankFn?: ConversionRankFn;
75
+ readonly constraintCompatibility?: ScopeResolver['constraintCompatibility'];
76
+ }): SymbolDefinition | undefined;
@@ -45,7 +45,10 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
45
45
  // the same name in a single class, choose the best match by
46
46
  // arity + argument types.
47
47
  if (fnDef === undefined) {
48
- fnDef = pickImplicitThisOverload(site, scopes, workspaceIndex, model, options.conversionRankFn);
48
+ fnDef = pickImplicitThisOverload(site, scopes, workspaceIndex, model, {
49
+ conversionRankFn: options.conversionRankFn,
50
+ constraintCompatibility: options.constraintCompatibility,
51
+ });
49
52
  }
50
53
  // Scope-chain callable lookup. First-match preserves scope-chain
51
54
  // precedence (local shadows import). When a conversion-rank function
@@ -63,7 +66,10 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
63
66
  if (fnDef !== undefined && options.conversionRankFn !== undefined) {
64
67
  const allCallables = findAllCallableBindingsInScope(site.inScope, site.name, scopes);
65
68
  if (allCallables.length > 1) {
66
- const narrowed = narrowOverloadCandidates(allCallables, site.arity, site.argumentTypes, options.conversionRankFn);
69
+ const narrowed = narrowOverloadCandidates(allCallables, site.arity, site.argumentTypes, {
70
+ conversionRankFn: options.conversionRankFn,
71
+ constraintCompatibility: options.constraintCompatibility,
72
+ });
67
73
  if (narrowed.length === 1) {
68
74
  fnDef = narrowed[0];
69
75
  }
@@ -97,36 +103,49 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
97
103
  argumentTypes: site.argumentTypes,
98
104
  atRange: { startLine: site.atRange.startLine, startCol: site.atRange.startCol },
99
105
  }, parsed, scopes, parsedFiles);
100
- // When ADL contributed no candidates, narrow ordinary candidates
101
- // with conversion-rank scoring when multiple overloads exist.
102
- // Single candidate or empty falls through to first-match.
106
+ const siteKey = `${parsed.filePath}:${site.atRange.startLine}:${site.atRange.startCol}`;
103
107
  if (adl === undefined || adl.length === 0) {
104
- if (ordinary.length <= 1 || options.conversionRankFn === undefined) {
108
+ // No ADL contribution. Default behavior: `ordinary[0]`
109
+ // scope-chain walk preserves local-shadows-import precedence.
110
+ //
111
+ // Narrowing kicks in when either disambiguation signal is
112
+ // present: any candidate carries `templateConstraints`
113
+ // (SFINAE / `requires`-clause guarded templates, #1579), OR
114
+ // a conversion-rank function is provided (#1606 / #1578).
115
+ // Both hooks are threaded into `narrowOverloadCandidates`
116
+ // via the unified `OverloadNarrowingHookCtx`.
117
+ const hasConstraints = ordinary.some((d) => d.templateConstraints !== undefined);
118
+ const canNarrow = hasConstraints || options.conversionRankFn !== undefined;
119
+ if (ordinary.length <= 1 || !canNarrow) {
105
120
  fnDef = ordinary[0];
106
121
  }
107
122
  else {
108
- const siteKey = `${parsed.filePath}:${site.atRange.startLine}:${site.atRange.startCol}`;
109
- const narrowed = narrowOverloadCandidates(ordinary, site.arity, site.argumentTypes, options.conversionRankFn);
123
+ const narrowed = narrowOverloadCandidates(ordinary, site.arity, site.argumentTypes, {
124
+ conversionRankFn: options.conversionRankFn,
125
+ constraintCompatibility: options.constraintCompatibility,
126
+ });
110
127
  if (narrowed.length === 1) {
111
128
  fnDef = narrowed[0];
112
129
  }
113
- else if (narrowed.length > 1) {
114
- // Multiple survivors — suppress when same-file (true
115
- // overloads), mirrors ADL merged-candidate behavior.
130
+ else if (narrowed.length === 0) {
131
+ handledSites.add(siteKey);
132
+ continue;
133
+ }
134
+ else {
135
+ // >1 survivors: same-file → suppress (true overloads,
136
+ // "degrade not lie" — no edge beats a wrong one, and
137
+ // SFINAE-ambiguous calls land here). Cross-file →
138
+ // first-match (shadowing semantics).
116
139
  const sameFile = narrowed.every((d) => d.filePath === narrowed[0].filePath);
117
140
  if (sameFile) {
118
141
  handledSites.add(siteKey);
119
142
  continue;
120
143
  }
121
- fnDef = ordinary[0]; // cross-file shadowing → first-match
122
- }
123
- else {
124
- fnDef = ordinary[0]; // narrowed empty → first-match
144
+ fnDef = ordinary[0];
125
145
  }
126
146
  }
127
147
  }
128
148
  else {
129
- const siteKey = `${parsed.filePath}:${site.atRange.startLine}:${site.atRange.startCol}`;
130
149
  const merged = [];
131
150
  const seenMerge = new Set();
132
151
  const push = (defs) => {
@@ -139,7 +158,10 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
139
158
  };
140
159
  push(ordinary);
141
160
  push(adl);
142
- const narrowed = narrowOverloadCandidates(merged, site.arity, site.argumentTypes, options.conversionRankFn);
161
+ const narrowed = narrowOverloadCandidates(merged, site.arity, site.argumentTypes, {
162
+ conversionRankFn: options.conversionRankFn,
163
+ constraintCompatibility: options.constraintCompatibility,
164
+ });
143
165
  if (narrowed.length === 1) {
144
166
  fnDef = narrowed[0];
145
167
  }
@@ -248,7 +270,9 @@ function pickUniqueGlobalCallable(name, model, scopes, callerFilePath, isFileLoc
248
270
  // best-rank candidate when exact-type or conversion-rank scoring can
249
271
  // disambiguate (e.g., `f(int)` vs `f(double)` called with `f(2.5)`).
250
272
  if (scopeDefs.length > 1) {
251
- const narrowed = narrowOverloadCandidates(scopeDefs, callArity, callArgTypes, conversionRankFn);
273
+ const narrowed = narrowOverloadCandidates(scopeDefs, callArity, callArgTypes, {
274
+ conversionRankFn,
275
+ });
252
276
  if (narrowed.length === 1)
253
277
  return narrowed[0];
254
278
  }
@@ -286,7 +310,9 @@ function pickUniqueGlobalCallable(name, model, scopes, callerFilePath, isFileLoc
286
310
  }
287
311
  // Same argument-type + conversion-rank narrowing for the model pool.
288
312
  if (defs.length > 1) {
289
- const narrowed = narrowOverloadCandidates(defs, callArity, callArgTypes, conversionRankFn);
313
+ const narrowed = narrowOverloadCandidates(defs, callArity, callArgTypes, {
314
+ conversionRankFn,
315
+ });
290
316
  if (narrowed.length === 1)
291
317
  return narrowed[0];
292
318
  }
@@ -345,7 +371,7 @@ function pickConstructorOrClass(classDef, workspaceIndex) {
345
371
  * Exported for unit testing — language-agnostic logic, exercised
346
372
  * via synthetic stubs in `pick-implicit-this-overload.test.ts`. The
347
373
  * production call site is `applyFreeCallFallback` immediately above. */
348
- export function pickImplicitThisOverload(site, scopes, workspaceIndex, model, conversionRankFn) {
374
+ export function pickImplicitThisOverload(site, scopes, workspaceIndex, model, hookCtx) {
349
375
  // Find the enclosing Class scope by walking parents.
350
376
  let curId = site.inScope;
351
377
  let classScopeId;
@@ -374,7 +400,10 @@ export function pickImplicitThisOverload(site, scopes, workspaceIndex, model, co
374
400
  // ambiguous narrowing (multiple compatible candidates with no
375
401
  // disambiguating signal) leaves the call unresolved rather than
376
402
  // routing to an arbitrary first overload by registration order.
377
- const candidates = narrowOverloadCandidates(overloads, site.arity, site.argumentTypes, conversionRankFn);
403
+ const candidates = narrowOverloadCandidates(overloads, site.arity, site.argumentTypes, {
404
+ conversionRankFn: hookCtx?.conversionRankFn,
405
+ constraintCompatibility: hookCtx?.constraintCompatibility,
406
+ });
378
407
  if (candidates.length !== 1)
379
408
  return undefined;
380
409
  return candidates[0];
@@ -25,14 +25,19 @@
25
25
  * counts as a match. Mismatches disqualify. A non-empty typed
26
26
  * result wins; otherwise return the arity-filtered candidates.
27
27
  * 4b. When the exact-type filter from step 4 returns empty AND a
28
- * `conversionRankFn` is provided, rank candidates via pairwise
29
- * dominance comparison (ISO C++ [over.ics.rank]): F1 beats F2
30
- * only when F1 is not worse for every arg and better for at
31
- * least one. Non-dominated candidates are returned; multiple
32
- * survivors are genuinely ambiguous.
28
+ * `conversionRankFn` is provided (via `hookCtx`), rank candidates
29
+ * via pairwise dominance comparison (ISO C++ [over.ics.rank]):
30
+ * F1 beats F2 only when F1 is not worse for every arg and better
31
+ * for at least one. Non-dominated candidates are returned;
32
+ * multiple survivors are genuinely ambiguous.
33
+ * 4c. Final per-candidate constraint filter (SFINAE / `requires`).
34
+ * When `constraintCompatibility` is provided via `hookCtx`, drop
35
+ * candidates whose template constraints provably fail at the
36
+ * call site. Three-valued; `'unknown'` keeps the candidate
37
+ * (monotonicity).
33
38
  * 5. Empty input returns empty output.
34
39
  */
35
- import type { SymbolDefinition } from '../../../../_shared/index.js';
40
+ import type { ArityVerdict, Callsite, ConstraintContext, SymbolDefinition } from '../../../../_shared/index.js';
36
41
  /**
37
42
  * Per-slot conversion-rank function. Returns a numeric cost for
38
43
  * converting `argType` to `paramType`:
@@ -45,7 +50,25 @@ import type { SymbolDefinition } from '../../../../_shared/index.js';
45
50
  * on normalized type strings (output of the language's type normalizer).
46
51
  */
47
52
  export type ConversionRankFn = (argType: string, paramType: string) => number;
48
- export declare function narrowOverloadCandidates(overloads: readonly SymbolDefinition[], argCount: number | undefined, argTypes: readonly string[] | undefined, conversionRankFn?: ConversionRankFn): readonly SymbolDefinition[];
53
+ /**
54
+ * Optional hook bundle for narrowing extension points. Threaded in
55
+ * from `pickOverload` / `pickImplicitThisOverload` so per-language
56
+ * narrowing can layer in conversion-rank scoring (#1606) and
57
+ * constraint filtering (#1579) without changing the call signature
58
+ * at every site. Each hook is independently optional — leaving both
59
+ * undefined preserves the legacy arity + exact-type behavior.
60
+ */
61
+ export interface OverloadNarrowingHookCtx {
62
+ /** Conversion-rank scoring fallback (step 4b). Engages when the
63
+ * exact-type filter rejects every candidate. */
64
+ readonly conversionRankFn?: ConversionRankFn;
65
+ /** Constraint filter (step 4c). Drops candidates whose template
66
+ * guards (SFINAE `enable_if_t`, C++20 `requires`, future Rust
67
+ * trait bounds, etc.) provably fail at the call site. Three-valued
68
+ * — `'unknown'` keeps the candidate (monotonicity). */
69
+ readonly constraintCompatibility?: (callsite: Callsite, def: SymbolDefinition, ctx: ConstraintContext) => ArityVerdict;
70
+ }
71
+ export declare function narrowOverloadCandidates(overloads: readonly SymbolDefinition[], argCount: number | undefined, argTypes: readonly string[] | undefined, hookCtx?: OverloadNarrowingHookCtx): readonly SymbolDefinition[];
49
72
  /**
50
73
  * Detect when >1 candidate share identical `parameterTypes` after the
51
74
  * per-language normalizer has collapsed distinct underlying types. This
@@ -25,14 +25,19 @@
25
25
  * counts as a match. Mismatches disqualify. A non-empty typed
26
26
  * result wins; otherwise return the arity-filtered candidates.
27
27
  * 4b. When the exact-type filter from step 4 returns empty AND a
28
- * `conversionRankFn` is provided, rank candidates via pairwise
29
- * dominance comparison (ISO C++ [over.ics.rank]): F1 beats F2
30
- * only when F1 is not worse for every arg and better for at
31
- * least one. Non-dominated candidates are returned; multiple
32
- * survivors are genuinely ambiguous.
28
+ * `conversionRankFn` is provided (via `hookCtx`), rank candidates
29
+ * via pairwise dominance comparison (ISO C++ [over.ics.rank]):
30
+ * F1 beats F2 only when F1 is not worse for every arg and better
31
+ * for at least one. Non-dominated candidates are returned;
32
+ * multiple survivors are genuinely ambiguous.
33
+ * 4c. Final per-candidate constraint filter (SFINAE / `requires`).
34
+ * When `constraintCompatibility` is provided via `hookCtx`, drop
35
+ * candidates whose template constraints provably fail at the
36
+ * call site. Three-valued; `'unknown'` keeps the candidate
37
+ * (monotonicity).
33
38
  * 5. Empty input returns empty output.
34
39
  */
35
- export function narrowOverloadCandidates(overloads, argCount, argTypes, conversionRankFn) {
40
+ export function narrowOverloadCandidates(overloads, argCount, argTypes, hookCtx) {
36
41
  if (overloads.length === 0)
37
42
  return [];
38
43
  const arityMatches = argCount === undefined
@@ -67,6 +72,7 @@ export function narrowOverloadCandidates(overloads, argCount, argTypes, conversi
67
72
  // args).
68
73
  const anyUnknownBounds = overloads.some((d) => d.parameterCount === undefined && d.requiredParameterCount === undefined);
69
74
  const candidates = arityMatches.length > 0 ? arityMatches : anyUnknownBounds ? overloads : [];
75
+ let result = candidates;
70
76
  if (argTypes !== undefined && argTypes.length > 0) {
71
77
  const typed = candidates.filter((d) => {
72
78
  const params = d.parameterTypes;
@@ -80,21 +86,46 @@ export function narrowOverloadCandidates(overloads, argCount, argTypes, conversi
80
86
  }
81
87
  return true;
82
88
  });
83
- if (typed.length > 0)
84
- return typed;
85
- // ── Conversion-rank scoring (step 4b) ──────────────────────────
86
- // The exact-type filter above rejected every candidate. When a
87
- // per-language conversion-rank function is available, rank via
88
- // pairwise dominance: F1 beats F2 only when F1 is not worse for
89
- // every arg and better for at least one. Non-dominated candidates
90
- // are returned; multiple survivors are genuinely ambiguous.
91
- if (conversionRankFn !== undefined) {
92
- const ranked = rankByConversion(candidates, argTypes, conversionRankFn);
89
+ if (typed.length > 0) {
90
+ result = typed;
91
+ }
92
+ else if (hookCtx?.conversionRankFn !== undefined) {
93
+ // ── Conversion-rank scoring (step 4b) ──────────────────────────
94
+ // The exact-type filter rejected every candidate. Rank via
95
+ // pairwise dominance: F1 beats F2 only when F1 is not worse for
96
+ // every arg and better for at least one. Non-dominated candidates
97
+ // are returned; multiple survivors are genuinely ambiguous. When
98
+ // ranking also yields empty, fall through to the arity-filtered
99
+ // `candidates` set — matches pre-#1606 behavior.
100
+ const ranked = rankByConversion(candidates, argTypes, hookCtx.conversionRankFn);
93
101
  if (ranked.length > 0)
94
- return ranked;
102
+ result = ranked;
95
103
  }
96
104
  }
97
- return candidates;
105
+ // Constraint filter (step 4c; Tier-A — SFINAE / `requires` clauses).
106
+ // Runs after arity, exact-type, and conversion-rank filters so the
107
+ // hook only sees candidates already viable on the other axes.
108
+ // Three-valued: `'compatible'` and `'unknown'` keep the candidate
109
+ // (monotonicity — adding a predicate must never cause a wrong edge);
110
+ // only `'incompatible'` drops it. Candidates without
111
+ // `templateConstraints` are always kept.
112
+ //
113
+ // No fallback to the unconstrained set when this filter empties the
114
+ // candidate list: a fully-`'incompatible'` verdict is authoritative.
115
+ // The downstream `OVERLOAD_AMBIGUOUS` sentinel still guards the empty
116
+ // case, so a buggy hook that wrongly returns `'incompatible'` for
117
+ // every candidate degrades to today's "suppress edge" behavior rather
118
+ // than emitting a wrong edge.
119
+ if (hookCtx?.constraintCompatibility !== undefined && argCount !== undefined) {
120
+ const callsite = { arity: argCount };
121
+ const ctx = argTypes !== undefined ? { argumentTypes: argTypes } : {};
122
+ result = result.filter((def) => {
123
+ if (def.templateConstraints === undefined)
124
+ return true;
125
+ return hookCtx.constraintCompatibility(callsite, def, ctx) !== 'incompatible';
126
+ });
127
+ }
128
+ return result;
98
129
  }
99
130
  /**
100
131
  * Pairwise dominance comparison (ISO C++ [over.ics.rank]).
@@ -41,7 +41,7 @@ import type { WorkspaceResolutionIndex } from '../workspace-index.js';
41
41
  /** Subset of `ScopeResolver` consumed by this pass. Accepting the
42
42
  * subset rather than the full provider keeps tests and partial
43
43
  * refactors lighter — callers only need to populate what we read. */
44
- type ReceiverBoundProviderSubset = Pick<ScopeResolver, 'isSuperReceiver' | 'isSuperReceiverInContext' | 'fieldFallbackOnMethodLookup' | 'collapseMemberCallsByCallerTarget' | 'unwrapCollectionAccessor' | 'hoistTypeBindingsToModule' | 'resolveQualifiedReceiverMember' | 'resolveThisViaEnclosingClass' | 'conversionRankFn'>;
44
+ type ReceiverBoundProviderSubset = Pick<ScopeResolver, 'isSuperReceiver' | 'isSuperReceiverInContext' | 'fieldFallbackOnMethodLookup' | 'collapseMemberCallsByCallerTarget' | 'unwrapCollectionAccessor' | 'hoistTypeBindingsToModule' | 'resolveQualifiedReceiverMember' | 'resolveThisViaEnclosingClass' | 'conversionRankFn' | 'constraintCompatibility'>;
45
45
  export declare function emitReceiverBoundCalls(graph: KnowledgeGraph, scopes: ScopeResolutionIndexes, parsedFiles: readonly ParsedFile[], nodeLookup: GraphNodeLookup, handledSites: Set<string>, provider: ReceiverBoundProviderSubset, index: WorkspaceResolutionIndex, model: SemanticModel): number;
46
46
  /**
47
47
  * Sentinel returned by `pickOverload` when narrowing leaves >1 candidate