gitnexus 1.6.5 → 1.6.6-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/_shared/index.d.ts +1 -1
  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 +7 -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 +8 -0
  12. package/dist/core/ingestion/languages/cpp/arity-metadata.js +1 -1
  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 +70 -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/parsing-processor.js +33 -2
  24. package/dist/core/ingestion/scope-extractor.js +17 -0
  25. package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +21 -1
  26. package/dist/core/ingestion/scope-resolution/graph-bridge/ids.d.ts +1 -0
  27. package/dist/core/ingestion/scope-resolution/graph-bridge/ids.js +14 -0
  28. package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.js +12 -0
  29. package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.d.ts +11 -1
  30. package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +50 -21
  31. package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.d.ts +30 -7
  32. package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.js +49 -18
  33. package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.d.ts +1 -1
  34. package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +10 -4
  35. package/dist/core/ingestion/scope-resolution/pipeline/run.js +1 -0
  36. package/dist/core/ingestion/utils/template-arguments.d.ts +19 -0
  37. package/dist/core/ingestion/utils/template-arguments.js +30 -0
  38. package/package.json +1 -1
@@ -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
@@ -249,7 +249,10 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
249
249
  for (const ownerId of chain) {
250
250
  const methodOverloads = model.methods.lookupAllByOwner(ownerId, memberName);
251
251
  if (methodOverloads.length > 0) {
252
- const narrowed = narrowOverloadCandidates(methodOverloads, site.arity, site.argumentTypes, provider.conversionRankFn);
252
+ const narrowed = narrowOverloadCandidates(methodOverloads, site.arity, site.argumentTypes, {
253
+ conversionRankFn: provider.conversionRankFn,
254
+ constraintCompatibility: provider.constraintCompatibility,
255
+ });
253
256
  if (isOverloadAmbiguousAfterNormalization(narrowed, site.arity)) {
254
257
  ambiguous = true;
255
258
  break;
@@ -467,7 +470,7 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
467
470
  let memberDef;
468
471
  let ambiguous = false;
469
472
  for (const ownerId of chain) {
470
- const picked = pickOverload(ownerId, memberName, site, model, provider.conversionRankFn);
473
+ const picked = pickOverload(ownerId, memberName, site, model, provider);
471
474
  if (picked === OVERLOAD_AMBIGUOUS) {
472
475
  ambiguous = true;
473
476
  break;
@@ -518,7 +521,7 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
518
521
  * types when multiple overloads share the name. Falls back to the
519
522
  * first-seen def (legacy `findOwnedMember` semantics) when there's
520
523
  * no narrowing signal or when `argumentTypes` is unavailable. */
521
- function pickOverload(ownerId, memberName, site, model, conversionRankFn) {
524
+ function pickOverload(ownerId, memberName, site, model, provider) {
522
525
  const overloads = model.methods.lookupAllByOwner(ownerId, memberName);
523
526
  if (overloads.length === 0) {
524
527
  // Non-callable member (field / property / variable) — ACCESSES
@@ -528,7 +531,10 @@ function pickOverload(ownerId, memberName, site, model, conversionRankFn) {
528
531
  }
529
532
  if (overloads.length === 1)
530
533
  return overloads[0];
531
- const candidates = narrowOverloadCandidates(overloads, site.arity, site.argumentTypes, conversionRankFn);
534
+ const candidates = narrowOverloadCandidates(overloads, site.arity, site.argumentTypes, {
535
+ conversionRankFn: provider.conversionRankFn,
536
+ constraintCompatibility: provider.constraintCompatibility,
537
+ });
532
538
  // When narrowing leaves >1 candidate that share identical normalized
533
539
  // parameter-types (e.g., C++ `f(int)` vs `f(long)` both collapsed to
534
540
  // `['int']` by `normalizeCppParamType`), suppress the edge entirely.
@@ -250,6 +250,7 @@ export function runScopeResolution(input, provider) {
250
250
  isCallableVisibleFromCaller: provider.isCallableVisibleFromCaller,
251
251
  resolveAdlCandidates: provider.resolveAdlCandidates,
252
252
  conversionRankFn: provider.conversionRankFn,
253
+ constraintCompatibility: provider.constraintCompatibility,
253
254
  });
254
255
  const { emitted, skipped } = emitReferencesViaLookup(graph, indexes, referenceIndex, nodeLookup, handledSites);
255
256
  const importsEmitted = emitImportEdges(graph, indexes.imports, indexes.scopeTree, provider.importEdgeReason);
@@ -9,3 +9,22 @@
9
9
  export declare function extractTemplateArguments(text: string): string[] | undefined;
10
10
  export declare function stripTemplateArguments(text: string): string;
11
11
  export declare function templateArgumentsIdTag(templateArguments?: readonly string[]): string;
12
+ /**
13
+ * Stable short hash for the opaque `SymbolDefinition.templateConstraints`
14
+ * payload (issue #1579). Two function-template overloads with identical
15
+ * `parameterTypes` but mutually-exclusive SFINAE constraints
16
+ * (`enable_if_t<is_integral_v<T>>` vs `enable_if_t<is_floating_point_v<T>>`)
17
+ * must produce distinct graph node IDs so the constraint-filter step
18
+ * has two candidates to narrow between. Without this they collapse to
19
+ * a single Function node and the SFINAE golden case can only emit one
20
+ * edge regardless of resolver fixes.
21
+ *
22
+ * FNV-1a 32-bit, base36 encoded. Deterministic; non-cryptographic — the
23
+ * tag's job is collision-avoidance among same-name overloads in one
24
+ * file, not security.
25
+ */
26
+ export declare function constraintsHash(jsonText: string): string;
27
+ /** Build the `~c:<hash>` ID suffix from an opaque constraint payload.
28
+ * Returns empty string when the payload is absent so callers can
29
+ * string-concatenate unconditionally. */
30
+ export declare function templateConstraintsIdTag(payload: unknown): string;
@@ -63,3 +63,33 @@ export function templateArgumentsIdTag(templateArguments) {
63
63
  return '';
64
64
  return `~${templateArguments.join(',')}`;
65
65
  }
66
+ /**
67
+ * Stable short hash for the opaque `SymbolDefinition.templateConstraints`
68
+ * payload (issue #1579). Two function-template overloads with identical
69
+ * `parameterTypes` but mutually-exclusive SFINAE constraints
70
+ * (`enable_if_t<is_integral_v<T>>` vs `enable_if_t<is_floating_point_v<T>>`)
71
+ * must produce distinct graph node IDs so the constraint-filter step
72
+ * has two candidates to narrow between. Without this they collapse to
73
+ * a single Function node and the SFINAE golden case can only emit one
74
+ * edge regardless of resolver fixes.
75
+ *
76
+ * FNV-1a 32-bit, base36 encoded. Deterministic; non-cryptographic — the
77
+ * tag's job is collision-avoidance among same-name overloads in one
78
+ * file, not security.
79
+ */
80
+ export function constraintsHash(jsonText) {
81
+ let h = 0x811c9dc5;
82
+ for (let i = 0; i < jsonText.length; i++) {
83
+ h ^= jsonText.charCodeAt(i);
84
+ h = Math.imul(h, 0x01000193);
85
+ }
86
+ return (h >>> 0).toString(36);
87
+ }
88
+ /** Build the `~c:<hash>` ID suffix from an opaque constraint payload.
89
+ * Returns empty string when the payload is absent so callers can
90
+ * string-concatenate unconditionally. */
91
+ export function templateConstraintsIdTag(payload) {
92
+ if (payload === undefined || payload === null)
93
+ return '';
94
+ return `~c:${constraintsHash(JSON.stringify(payload))}`;
95
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.5",
3
+ "version": "1.6.6-rc.1",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",