gitnexus 1.6.5 → 1.6.6-rc.10

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 (81) 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 +37 -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/registries/lookup-core.d.ts +4 -2
  8. package/dist/_shared/scope-resolution/registries/lookup-core.d.ts.map +1 -1
  9. package/dist/_shared/scope-resolution/registries/lookup-core.js +8 -23
  10. package/dist/_shared/scope-resolution/registries/lookup-core.js.map +1 -1
  11. package/dist/_shared/scope-resolution/symbol-definition.d.ts +20 -0
  12. package/dist/_shared/scope-resolution/symbol-definition.d.ts.map +1 -1
  13. package/dist/cli/analyze.js +65 -3
  14. package/dist/cli/eval-server.d.ts +14 -3
  15. package/dist/cli/eval-server.js +79 -9
  16. package/dist/cli/index.js +3 -1
  17. package/dist/cli/serve.js +6 -1
  18. package/dist/cli/wiki.d.ts +1 -0
  19. package/dist/cli/wiki.js +32 -8
  20. package/dist/core/ingestion/language-provider.d.ts +29 -0
  21. package/dist/core/ingestion/languages/c-cpp.js +46 -0
  22. package/dist/core/ingestion/languages/cpp/arity-metadata.d.ts +17 -0
  23. package/dist/core/ingestion/languages/cpp/arity-metadata.js +51 -2
  24. package/dist/core/ingestion/languages/cpp/arity.d.ts +4 -1
  25. package/dist/core/ingestion/languages/cpp/arity.js +4 -1
  26. package/dist/core/ingestion/languages/cpp/captures.js +73 -0
  27. package/dist/core/ingestion/languages/cpp/constraint-extractor.d.ts +73 -0
  28. package/dist/core/ingestion/languages/cpp/constraint-extractor.js +308 -0
  29. package/dist/core/ingestion/languages/cpp/constraint-filter.d.ts +31 -0
  30. package/dist/core/ingestion/languages/cpp/constraint-filter.js +135 -0
  31. package/dist/core/ingestion/languages/cpp/scope-resolver.js +6 -0
  32. package/dist/core/ingestion/languages/cpp/type-classifier.d.ts +26 -0
  33. package/dist/core/ingestion/languages/cpp/type-classifier.js +49 -0
  34. package/dist/core/ingestion/model/field-registry.d.ts +17 -3
  35. package/dist/core/ingestion/model/field-registry.js +19 -4
  36. package/dist/core/ingestion/model/owned-members-lookup.d.ts +19 -0
  37. package/dist/core/ingestion/model/owned-members-lookup.js +43 -0
  38. package/dist/core/ingestion/model/symbol-table.d.ts +2 -1
  39. package/dist/core/ingestion/model/symbol-table.js +3 -0
  40. package/dist/core/ingestion/model/type-registry.d.ts +9 -0
  41. package/dist/core/ingestion/model/type-registry.js +18 -0
  42. package/dist/core/ingestion/parsing-processor.js +35 -2
  43. package/dist/core/ingestion/resolve-references.d.ts +3 -1
  44. package/dist/core/ingestion/resolve-references.js +5 -1
  45. package/dist/core/ingestion/scope-extractor.js +63 -0
  46. package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +21 -1
  47. package/dist/core/ingestion/scope-resolution/graph-bridge/ids.d.ts +1 -0
  48. package/dist/core/ingestion/scope-resolution/graph-bridge/ids.js +14 -0
  49. package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.js +12 -0
  50. package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.d.ts +11 -1
  51. package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +83 -28
  52. package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.d.ts +30 -7
  53. package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.js +49 -18
  54. package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.d.ts +1 -1
  55. package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +10 -4
  56. package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.d.ts +4 -2
  57. package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.js +48 -9
  58. package/dist/core/ingestion/scope-resolution/pipeline/run.js +3 -0
  59. package/dist/core/ingestion/utils/template-arguments.d.ts +19 -0
  60. package/dist/core/ingestion/utils/template-arguments.js +30 -0
  61. package/dist/core/ingestion/workers/parse-worker.d.ts +2 -1
  62. package/dist/core/ingestion/workers/parse-worker.js +1 -0
  63. package/dist/core/lbug/lbug-adapter.d.ts +3 -1
  64. package/dist/core/lbug/lbug-adapter.js +33 -19
  65. package/dist/core/lbug/lbug-config.d.ts +1 -1
  66. package/dist/core/lbug/lbug-config.js +1 -1
  67. package/dist/core/lbug/pool-adapter.d.ts +0 -7
  68. package/dist/core/lbug/pool-adapter.js +10 -35
  69. package/dist/core/lbug/query-params.d.ts +1 -0
  70. package/dist/core/lbug/query-params.js +21 -0
  71. package/dist/core/search/bm25-index.js +4 -6
  72. package/dist/core/wiki/generator.d.ts +13 -0
  73. package/dist/core/wiki/generator.js +45 -4
  74. package/dist/core/wiki/llm-client.d.ts +1 -1
  75. package/dist/core/wiki/llm-client.js +23 -6
  76. package/dist/mcp/local/local-backend.d.ts +18 -3
  77. package/dist/mcp/local/local-backend.js +105 -11
  78. package/dist/mcp/tools.js +10 -0
  79. package/dist/server/api.d.ts +3 -0
  80. package/dist/server/api.js +36 -24
  81. package/package.json +1 -1
@@ -22,6 +22,11 @@ import { isOverloadAmbiguousAfterNormalization, narrowOverloadCandidates, } from
22
22
  export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _referenceIndex, handledSites, model, workspaceIndex, options = {}) {
23
23
  let emitted = 0;
24
24
  const seen = new Set();
25
+ // Build an O(1) simple-name -> callable defs index over scopes.defs once
26
+ // per pass so pickUniqueGlobalCallable doesn't re-scan defs.byId.values()
27
+ // per call site. Same name + callable-kind filter that the previous scan
28
+ // applied (see pickUniqueGlobalCallable JSDoc). Cost: O(|defs|) once.
29
+ const globalCallablesBySimpleName = buildGlobalCallableIndex(scopes);
25
30
  for (const parsed of parsedFiles) {
26
31
  for (const site of parsed.referenceSites) {
27
32
  if (site.kind !== 'call')
@@ -45,7 +50,10 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
45
50
  // the same name in a single class, choose the best match by
46
51
  // arity + argument types.
47
52
  if (fnDef === undefined) {
48
- fnDef = pickImplicitThisOverload(site, scopes, workspaceIndex, model, options.conversionRankFn);
53
+ fnDef = pickImplicitThisOverload(site, scopes, workspaceIndex, model, {
54
+ conversionRankFn: options.conversionRankFn,
55
+ constraintCompatibility: options.constraintCompatibility,
56
+ });
49
57
  }
50
58
  // Scope-chain callable lookup. First-match preserves scope-chain
51
59
  // precedence (local shadows import). When a conversion-rank function
@@ -63,7 +71,10 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
63
71
  if (fnDef !== undefined && options.conversionRankFn !== undefined) {
64
72
  const allCallables = findAllCallableBindingsInScope(site.inScope, site.name, scopes);
65
73
  if (allCallables.length > 1) {
66
- const narrowed = narrowOverloadCandidates(allCallables, site.arity, site.argumentTypes, options.conversionRankFn);
74
+ const narrowed = narrowOverloadCandidates(allCallables, site.arity, site.argumentTypes, {
75
+ conversionRankFn: options.conversionRankFn,
76
+ constraintCompatibility: options.constraintCompatibility,
77
+ });
67
78
  if (narrowed.length === 1) {
68
79
  fnDef = narrowed[0];
69
80
  }
@@ -97,36 +108,49 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
97
108
  argumentTypes: site.argumentTypes,
98
109
  atRange: { startLine: site.atRange.startLine, startCol: site.atRange.startCol },
99
110
  }, 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.
111
+ const siteKey = `${parsed.filePath}:${site.atRange.startLine}:${site.atRange.startCol}`;
103
112
  if (adl === undefined || adl.length === 0) {
104
- if (ordinary.length <= 1 || options.conversionRankFn === undefined) {
113
+ // No ADL contribution. Default behavior: `ordinary[0]` —
114
+ // scope-chain walk preserves local-shadows-import precedence.
115
+ //
116
+ // Narrowing kicks in when either disambiguation signal is
117
+ // present: any candidate carries `templateConstraints`
118
+ // (SFINAE / `requires`-clause guarded templates, #1579), OR
119
+ // a conversion-rank function is provided (#1606 / #1578).
120
+ // Both hooks are threaded into `narrowOverloadCandidates`
121
+ // via the unified `OverloadNarrowingHookCtx`.
122
+ const hasConstraints = ordinary.some((d) => d.templateConstraints !== undefined);
123
+ const canNarrow = hasConstraints || options.conversionRankFn !== undefined;
124
+ if (ordinary.length <= 1 || !canNarrow) {
105
125
  fnDef = ordinary[0];
106
126
  }
107
127
  else {
108
- const siteKey = `${parsed.filePath}:${site.atRange.startLine}:${site.atRange.startCol}`;
109
- const narrowed = narrowOverloadCandidates(ordinary, site.arity, site.argumentTypes, options.conversionRankFn);
128
+ const narrowed = narrowOverloadCandidates(ordinary, site.arity, site.argumentTypes, {
129
+ conversionRankFn: options.conversionRankFn,
130
+ constraintCompatibility: options.constraintCompatibility,
131
+ });
110
132
  if (narrowed.length === 1) {
111
133
  fnDef = narrowed[0];
112
134
  }
113
- else if (narrowed.length > 1) {
114
- // Multiple survivors — suppress when same-file (true
115
- // overloads), mirrors ADL merged-candidate behavior.
135
+ else if (narrowed.length === 0) {
136
+ handledSites.add(siteKey);
137
+ continue;
138
+ }
139
+ else {
140
+ // >1 survivors: same-file → suppress (true overloads,
141
+ // "degrade not lie" — no edge beats a wrong one, and
142
+ // SFINAE-ambiguous calls land here). Cross-file →
143
+ // first-match (shadowing semantics).
116
144
  const sameFile = narrowed.every((d) => d.filePath === narrowed[0].filePath);
117
145
  if (sameFile) {
118
146
  handledSites.add(siteKey);
119
147
  continue;
120
148
  }
121
- fnDef = ordinary[0]; // cross-file shadowing → first-match
122
- }
123
- else {
124
- fnDef = ordinary[0]; // narrowed empty → first-match
149
+ fnDef = ordinary[0];
125
150
  }
126
151
  }
127
152
  }
128
153
  else {
129
- const siteKey = `${parsed.filePath}:${site.atRange.startLine}:${site.atRange.startCol}`;
130
154
  const merged = [];
131
155
  const seenMerge = new Set();
132
156
  const push = (defs) => {
@@ -139,7 +163,10 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
139
163
  };
140
164
  push(ordinary);
141
165
  push(adl);
142
- const narrowed = narrowOverloadCandidates(merged, site.arity, site.argumentTypes, options.conversionRankFn);
166
+ const narrowed = narrowOverloadCandidates(merged, site.arity, site.argumentTypes, {
167
+ conversionRankFn: options.conversionRankFn,
168
+ constraintCompatibility: options.constraintCompatibility,
169
+ });
143
170
  if (narrowed.length === 1) {
144
171
  fnDef = narrowed[0];
145
172
  }
@@ -165,7 +192,7 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
165
192
  // the caller does not import the target package. Same-package calls are
166
193
  // usually caught by nearest-scope lookup before reaching here.
167
194
  if (fnDef === undefined && options.allowGlobalFallback === true) {
168
- fnDef = pickUniqueGlobalCallable(site.name, model, scopes, parsed.filePath, options.isFileLocalDef, site.arity, options.isCallableVisibleFromCaller !== undefined
195
+ fnDef = pickUniqueGlobalCallable(site.name, model, globalCallablesBySimpleName, parsed.filePath, options.isFileLocalDef, site.arity, options.isCallableVisibleFromCaller !== undefined
169
196
  ? (candidate) => options.isCallableVisibleFromCaller({
170
197
  callerParsed: parsed,
171
198
  candidate,
@@ -205,15 +232,36 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
205
232
  }
206
233
  return emitted;
207
234
  }
208
- function pickUniqueGlobalCallable(name, model, scopes, callerFilePath, isFileLocalDef, callArity, isCallerVisible, callArgTypes, conversionRankFn) {
209
- const scopeDefs = [];
210
- const scopeSeen = new Set();
235
+ /**
236
+ * Build a `simpleName -> callable defs` index from `scopes.defs` once per
237
+ * pass. Mirrors the filter the old per-site scan applied: Function /
238
+ * Method / Constructor, keyed by the last `.`-segment of `qualifiedName`
239
+ * (falling back to the qualifiedName itself when undotted). Used by
240
+ * `pickUniqueGlobalCallable` so every free-call fallback site is O(1)
241
+ * instead of O(|defs|).
242
+ */
243
+ function buildGlobalCallableIndex(scopes) {
244
+ const out = new Map();
211
245
  for (const def of scopes.defs.byId.values()) {
212
- const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName;
213
- if (simple !== name)
214
- continue;
215
246
  if (def.type !== 'Function' && def.type !== 'Method' && def.type !== 'Constructor')
216
247
  continue;
248
+ const qualified = def.qualifiedName;
249
+ if (qualified === undefined || qualified.length === 0)
250
+ continue;
251
+ const dot = qualified.lastIndexOf('.');
252
+ const simple = dot === -1 ? qualified : qualified.slice(dot + 1);
253
+ const bucket = out.get(simple);
254
+ if (bucket)
255
+ bucket.push(def);
256
+ else
257
+ out.set(simple, [def]);
258
+ }
259
+ return out;
260
+ }
261
+ function pickUniqueGlobalCallable(name, model, globalCallablesBySimpleName, callerFilePath, isFileLocalDef, callArity, isCallerVisible, callArgTypes, conversionRankFn) {
262
+ const scopeDefs = [];
263
+ const scopeSeen = new Set();
264
+ for (const def of globalCallablesBySimpleName.get(name) ?? []) {
217
265
  // Skip file-local defs (e.g. C `static` functions) that live in a
218
266
  // different file from the caller — they are logically invisible.
219
267
  if (isFileLocalDef !== undefined && def.filePath !== callerFilePath && isFileLocalDef(def)) {
@@ -248,7 +296,9 @@ function pickUniqueGlobalCallable(name, model, scopes, callerFilePath, isFileLoc
248
296
  // best-rank candidate when exact-type or conversion-rank scoring can
249
297
  // disambiguate (e.g., `f(int)` vs `f(double)` called with `f(2.5)`).
250
298
  if (scopeDefs.length > 1) {
251
- const narrowed = narrowOverloadCandidates(scopeDefs, callArity, callArgTypes, conversionRankFn);
299
+ const narrowed = narrowOverloadCandidates(scopeDefs, callArity, callArgTypes, {
300
+ conversionRankFn,
301
+ });
252
302
  if (narrowed.length === 1)
253
303
  return narrowed[0];
254
304
  }
@@ -286,7 +336,9 @@ function pickUniqueGlobalCallable(name, model, scopes, callerFilePath, isFileLoc
286
336
  }
287
337
  // Same argument-type + conversion-rank narrowing for the model pool.
288
338
  if (defs.length > 1) {
289
- const narrowed = narrowOverloadCandidates(defs, callArity, callArgTypes, conversionRankFn);
339
+ const narrowed = narrowOverloadCandidates(defs, callArity, callArgTypes, {
340
+ conversionRankFn,
341
+ });
290
342
  if (narrowed.length === 1)
291
343
  return narrowed[0];
292
344
  }
@@ -345,7 +397,7 @@ function pickConstructorOrClass(classDef, workspaceIndex) {
345
397
  * Exported for unit testing — language-agnostic logic, exercised
346
398
  * via synthetic stubs in `pick-implicit-this-overload.test.ts`. The
347
399
  * production call site is `applyFreeCallFallback` immediately above. */
348
- export function pickImplicitThisOverload(site, scopes, workspaceIndex, model, conversionRankFn) {
400
+ export function pickImplicitThisOverload(site, scopes, workspaceIndex, model, hookCtx) {
349
401
  // Find the enclosing Class scope by walking parents.
350
402
  let curId = site.inScope;
351
403
  let classScopeId;
@@ -374,7 +426,10 @@ export function pickImplicitThisOverload(site, scopes, workspaceIndex, model, co
374
426
  // ambiguous narrowing (multiple compatible candidates with no
375
427
  // disambiguating signal) leaves the call unresolved rather than
376
428
  // routing to an arbitrary first overload by registration order.
377
- const candidates = narrowOverloadCandidates(overloads, site.arity, site.argumentTypes, conversionRankFn);
429
+ const candidates = narrowOverloadCandidates(overloads, site.arity, site.argumentTypes, {
430
+ conversionRankFn: hookCtx?.conversionRankFn,
431
+ constraintCompatibility: hookCtx?.constraintCompatibility,
432
+ });
378
433
  if (candidates.length !== 1)
379
434
  return undefined;
380
435
  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.
@@ -18,8 +18,8 @@
18
18
  * undefined `ownerId` is reachable via either:
19
19
  * - `model.methods.lookupAllByOwner(ownerId, simpleName)` — if the
20
20
  * def is a Method / Function / Constructor, OR
21
- * - `model.fields.lookupFieldByOwner(ownerId, simpleName)` — if the
22
- * def is a Property / Variable.
21
+ * - `model.fields.lookupAllByOwner(ownerId, simpleName)` — if the
22
+ * def is a Property / Variable / Const / Static.
23
23
  *
24
24
  * This invariant is the foundation of Contract Invariant I9
25
25
  * (`contract/scope-resolver.ts`): scope-resolution passes MUST read
@@ -47,6 +47,8 @@ export interface ReconcileStats {
47
47
  readonly methodsRegistered: number;
48
48
  /** Property/Variable defs registered into FieldRegistry. */
49
49
  readonly fieldsRegistered: number;
50
+ /** Class-like nested type defs registered into TypeRegistry by owner. */
51
+ readonly nestedTypesRegistered: number;
50
52
  /** Defs already present (idempotent skip). */
51
53
  readonly skippedAlreadyPresent: number;
52
54
  }
@@ -18,8 +18,8 @@
18
18
  * undefined `ownerId` is reachable via either:
19
19
  * - `model.methods.lookupAllByOwner(ownerId, simpleName)` — if the
20
20
  * def is a Method / Function / Constructor, OR
21
- * - `model.fields.lookupFieldByOwner(ownerId, simpleName)` — if the
22
- * def is a Property / Variable.
21
+ * - `model.fields.lookupAllByOwner(ownerId, simpleName)` — if the
22
+ * def is a Property / Variable / Const / Static.
23
23
  *
24
24
  * This invariant is the foundation of Contract Invariant I9
25
25
  * (`contract/scope-resolver.ts`): scope-resolution passes MUST read
@@ -41,9 +41,25 @@
41
41
  * follow-up plan.
42
42
  */
43
43
  import { simpleQualifiedName } from '../graph-bridge/ids.js';
44
+ const NESTED_TYPE_KINDS = new Set([
45
+ 'Class',
46
+ 'Interface',
47
+ 'Enum',
48
+ 'Struct',
49
+ 'Union',
50
+ 'Trait',
51
+ 'TypeAlias',
52
+ 'Typedef',
53
+ 'Record',
54
+ 'Delegate',
55
+ 'Annotation',
56
+ 'Template',
57
+ 'Namespace',
58
+ ]);
44
59
  export function reconcileOwnership(parsedFiles, model) {
45
60
  let methodsRegistered = 0;
46
61
  let fieldsRegistered = 0;
62
+ let nestedTypesRegistered = 0;
47
63
  let skippedAlreadyPresent = 0;
48
64
  for (const parsed of parsedFiles) {
49
65
  for (const def of parsed.localDefs) {
@@ -62,18 +78,30 @@ export function reconcileOwnership(parsedFiles, model) {
62
78
  model.methods.register(ownerId, simple, def);
63
79
  methodsRegistered++;
64
80
  }
65
- else if (def.type === 'Property' || def.type === 'Variable') {
66
- const existing = model.fields.lookupFieldByOwner(ownerId, simple);
67
- if (existing !== undefined && existing.nodeId === def.nodeId) {
81
+ else if (def.type === 'Property' ||
82
+ def.type === 'Variable' ||
83
+ def.type === 'Const' ||
84
+ def.type === 'Static') {
85
+ const existing = model.fields.lookupAllByOwner(ownerId, simple);
86
+ if (existing.some((e) => e.nodeId === def.nodeId)) {
68
87
  skippedAlreadyPresent++;
69
88
  continue;
70
89
  }
71
90
  model.fields.register(ownerId, simple, def);
72
91
  fieldsRegistered++;
73
92
  }
93
+ else if (NESTED_TYPE_KINDS.has(def.type)) {
94
+ const existing = model.types.lookupAllByOwner(ownerId, simple);
95
+ if (existing.some((e) => e.nodeId === def.nodeId)) {
96
+ skippedAlreadyPresent++;
97
+ continue;
98
+ }
99
+ model.types.registerByOwner(ownerId, simple, def);
100
+ nestedTypesRegistered++;
101
+ }
74
102
  }
75
103
  }
76
- return { methodsRegistered, fieldsRegistered, skippedAlreadyPresent };
104
+ return { methodsRegistered, fieldsRegistered, nestedTypesRegistered, skippedAlreadyPresent };
77
105
  }
78
106
  /**
79
107
  * Debug-mode parity validator. Runs only when
@@ -111,14 +139,25 @@ export function validateOwnershipParity(parsedFiles, model, onWarn) {
111
139
  mismatches++;
112
140
  }
113
141
  }
114
- else if (def.type === 'Property' || def.type === 'Variable') {
115
- const found = model.fields.lookupFieldByOwner(ownerId, simple);
116
- if (found === undefined || found.nodeId !== def.nodeId) {
142
+ else if (def.type === 'Property' ||
143
+ def.type === 'Variable' ||
144
+ def.type === 'Const' ||
145
+ def.type === 'Static') {
146
+ const found = model.fields.lookupAllByOwner(ownerId, simple);
147
+ if (!found.some((d) => d.nodeId === def.nodeId)) {
117
148
  onWarn(`semantic-model parity: ${def.type} ${def.nodeId} (${parsed.filePath}) ` +
118
149
  `owned by ${ownerId} as "${simple}" not in FieldRegistry`);
119
150
  mismatches++;
120
151
  }
121
152
  }
153
+ else if (NESTED_TYPE_KINDS.has(def.type)) {
154
+ const found = model.types.lookupAllByOwner(ownerId, simple);
155
+ if (!found.some((d) => d.nodeId === def.nodeId)) {
156
+ onWarn(`semantic-model parity: ${def.type} ${def.nodeId} (${parsed.filePath}) ` +
157
+ `owned by ${ownerId} as "${simple}" not in TypeRegistry owner index`);
158
+ mismatches++;
159
+ }
160
+ }
122
161
  }
123
162
  }
124
163
  return mismatches;
@@ -22,6 +22,7 @@
22
22
  *
23
23
  * Plan: `docs/plans/2026-04-20-001-refactor-emit-pipeline-generalization-plan.md`.
24
24
  */
25
+ import { lookupOwnedMembersByOwner } from '../../model/owned-members-lookup.js';
25
26
  import { reconcileOwnership, validateOwnershipParity } from './reconcile-ownership.js';
26
27
  import { validateBindingsImmutability } from './validate-bindings-immutability.js';
27
28
  import { extractParsedFile } from '../../scope-extractor-bridge.js';
@@ -236,6 +237,7 @@ export function runScopeResolution(input, provider) {
236
237
  const { referenceIndex, stats: resolveStats } = resolveReferenceSites({
237
238
  scopes: indexes,
238
239
  providers: registryProviders,
240
+ ownedMembersByOwner: (ownerDefId, memberName) => lookupOwnedMembersByOwner(readonlyModel, ownerDefId, memberName),
239
241
  });
240
242
  const tResolve = PROF ? process.hrtime.bigint() : 0n;
241
243
  // ── Phase 4: emit graph edges (LOAD-BEARING ORDER — see I1) ────────────
@@ -250,6 +252,7 @@ export function runScopeResolution(input, provider) {
250
252
  isCallableVisibleFromCaller: provider.isCallableVisibleFromCaller,
251
253
  resolveAdlCandidates: provider.resolveAdlCandidates,
252
254
  conversionRankFn: provider.conversionRankFn,
255
+ constraintCompatibility: provider.constraintCompatibility,
253
256
  });
254
257
  const { emitted, skipped } = emitReferencesViaLookup(graph, indexes, referenceIndex, nodeLookup, handledSites);
255
258
  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
+ }
@@ -3,7 +3,7 @@ import type { ExtractedHeritage } from '../model/heritage-map.js';
3
3
  import { type MixedChainStep } from '../utils/call-analysis.js';
4
4
  import type { ConstructorBinding } from '../type-env.js';
5
5
  import type { NamedBinding } from '../named-bindings/types.js';
6
- import type { NodeLabel } from '../../../_shared/index.js';
6
+ import type { NodeLabel, ParameterTypeClass } from '../../../_shared/index.js';
7
7
  import type { ParsedFile } from '../../../_shared/index.js';
8
8
  interface ParsedNode {
9
9
  id: string;
@@ -38,6 +38,7 @@ interface ParsedSymbol {
38
38
  parameterCount?: number;
39
39
  requiredParameterCount?: number;
40
40
  parameterTypes?: string[];
41
+ parameterTypeClasses?: ParameterTypeClass[];
41
42
  returnType?: string;
42
43
  declaredType?: string;
43
44
  templateArguments?: string[];
@@ -1801,6 +1801,7 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
1801
1801
  parameterCount: methodProps.parameterCount,
1802
1802
  requiredParameterCount: methodProps.requiredParameterCount,
1803
1803
  parameterTypes: methodProps.parameterTypes,
1804
+ parameterTypeClasses: methodProps.parameterTypeClasses,
1804
1805
  returnType: methodProps.returnType,
1805
1806
  ...(declaredType !== undefined ? { declaredType } : {}),
1806
1807
  ...(classTemplateArguments !== undefined && classTemplateArguments.length > 0