/** * `ScopeResolver` — the per-language contract consumed by the generic * scope-resolution orchestrator (`runScopeResolution`). * * ## Migration cookbook (next language) * * To add a language to the registry-primary path: * * 1. Implement `ScopeResolver` in * `gitnexus/src/core/ingestion/languages//scope-resolver.ts`. * Nine required fields (language, languageProvider, * importEdgeReason, resolveImportTarget, mergeBindings, * arityCompatibility, buildMro, populateOwners, isSuperReceiver) * plus optional toggles / hooks: * - propagatesReturnTypesAcrossImports (default true) * - fieldFallbackOnMethodLookup (default true — turn OFF for * statically-typed languages; the heuristic over-connects) * - unwrapCollectionAccessor — property-style collection views * - collapseMemberCallsByCallerTarget — one edge per caller/target * - populateNamespaceSiblings — cross-file implicit visibility * - hoistTypeBindingsToModule — enable ONLY when method return * types are stored on the enclosing Module scope; most * languages attach them to the class scope and leave this off * 2. Export a thin entry point: * `runYourLangScopeResolution(input) = runScopeResolution(input, yourScopeResolver)`. * 3. Register the provider in * `gitnexus/src/core/ingestion/scope-resolution/pipeline/registry.ts` * (the `SCOPE_RESOLVERS` map). That registration is all it takes — the * `scopeResolutionPhase` runs every registered resolver. * 4. Verify the resolver integration test at * `gitnexus/test/integration/resolvers/.test.ts` passes (it runs * in the standard test suite). Scope-resolution is the only resolution * path — the legacy call-resolution DAG was removed in RING4-1 #942. * * No new pipeline phase, no orchestrator copy-paste, no workflow * change. The generic `scopeResolutionPhase` auto-discovers everything via * the `SCOPE_RESOLVERS` map. * * ## ScopeResolver vs LanguageProvider * * The codebase has two provider contracts. Their lifecycles differ: * * - `LanguageProvider` (`language-provider.ts`) is the * **parsing-side** contract — how to emit captures, classify * scopes, interpret imports / typeBindings. ~40 fields covering * both legacy and new pipelines. Consumed by `ScopeExtractor`, * once per file at extract time. * - `ScopeResolver` (this file) is the **emit-side** contract — how * the resolution pipeline dispatches references to graph edges. * 8 fields total. Consumed by `runScopeResolution`, once per * workspace at resolve time. * * They share three concept names (`arityCompatibility`, `mergeBindings`, * `resolveImportTarget`) because the emit pipeline reuses a few * finalize hooks. Per-language wiring passes the SAME function * reference through both interfaces — no second copy of the logic. * Rationale for not collapsing: lifecycle separation, and merging * would create a god-interface complicating future migrations. * * ## Reference implementation * * `gitnexus/src/core/ingestion/languages/python/scope-resolver.ts` — * `pythonScopeResolver` is the canonical example. Read that file when * migrating a new language; this interface lists the fields that * implementation populates. * * ## Contract Invariants the orchestrator depends on * * These are non-obvious behaviors that the orchestrator and the * existing Python + C# resolvers depend on. Future implementers will * break them silently if not documented. * * - **I1 — Phase 4 emission order is load-bearing.** `emitReceiverBoundCalls` * runs FIRST (populates `handledSites`), then `emitFreeCallFallback`, * then `emitReferencesViaLookup` (consumes `handledSites` as a skip * set), then `emitImportEdges`. Reordering breaks same-name collision * resolution: the shared lookup can mis-resolve `app_metrics.get_metrics()` * to a same-named local function, and only the precise per-receiver * pass running first prevents the wrong edge. * * - **I2 — `handledSites` semantics.** A site is added to * `handledSites` IFF a `tryEmitEdge` call returned `true` for it. * Sites a pass touched but couldn't resolve do NOT get marked — * they still get a chance from the shared resolver. Exception: * the free-call fallback marks the site unconditionally after * attempting emission (even on dedup-collapse), because the * per-(caller, target) collapse semantics require multiple call * sites in the same caller body not produce multiple edges. * `preEmitInheritanceEdges` also pre-marks every `inherits` site so * the generic bridge cannot remap class heritage into method-owned * EXTENDS edges via `resolveCallerGraphId`. * * - **I3 — `propagateImportedReturnTypes` mutation timing + ordering.** * The pass mutates `Scope.typeBindings` (a plain `new Map(...)` from * `draftToScope`, NOT frozen). It MUST run AFTER `finalizeScopeModel` * (so `indexes.bindings` is populated) and BEFORE * `resolveReferenceSites` (so resolution sees the propagated types). * The pass also re-runs `followChainPostFinalize` on every scope's * typeBindings because scope-extractor's pass-4 already ran and * missed any chain whose terminal lives in a foreign file. * Within the pass, files are walked in `indexes.sccs` reverse- * topological order (leaves first) so multi-hop alias chains * (e.g. `models.User → service.user → app.user`) collapse to the * terminal class in a single pass — every importer sees its * source's already-chain-followed typeBindings. Cyclic SCCs reach * a partial fixpoint within a single pass without iterating to * convergence; `ts-circular` only asserts pipeline-no-throw. * * - **I4 — `emitReceiverBoundCalls` case order.** Cases are evaluated * in this order; the FIRST that emits an edge wins: * 1. super branch (`provider.isSuperReceiver(receiverName)`) * 2. Case 0 compound (`receiverName` has `.` or `(`) * 3. Case 1 namespace-receiver * 4. Case 2 class-name receiver * 5. Case 3 dotted typeBinding for namespace prefix * 6. Case 3b chain-typebinding (compound resolver) * 7. Case 4 simple typeBinding (MRO walk + findOwnedMember) * Reordering or merging cases changes resolution semantics. The * numbering is part of the contract — keep the comments. * * - **I5 — Pre-seeding `seen` from `referenceIndex` is forbidden.** * Earlier versions of the receiver-bound pass pre-populated `seen` * to avoid double-emit. After Phase 4 was reordered, pre-seeding * became actively harmful: it suppresses correct emissions for * sites the shared resolver happened to resolve to a wrong target. * The orchestrator MUST NOT pre-seed. * * - **I6 — `Scope.typeBindings` is mutable post-finalize.** `draftToScope` * (in `scope-extractor.ts`) builds `typeBindings` as a plain * `new Map(...)` — not frozen, intentionally. Passes below rely on * this. Do NOT freeze `typeBindings` in any downstream refactor. * * - **I7 — `ScopeResolver` and `LanguageProvider` are distinct contracts.** * Python and C# pass the SAME function reference through both * interfaces where they share a hook name — no second copy of the * logic. Rationale for not collapsing them: lifecycles differ * (parsing-side runs once per file at extract time, emit-side runs * once per workspace at resolve time), and merging would create a * god-interface that complicates future migrations. * * - **I8 — Binding-channel lifecycle.** Post-finalize binding lookup * fans across several channels (`lookupBindingsAt` / * `findReceiverTypeBinding` consult them in precedence order): * `indexes.bindings` (frozen finalize output), `Scope.bindings` * (lexical local, first-tier shadowing), `indexes.bindingAugmentations` * (per-scope append-only), `indexes.workspaceFqnBindings` + * `indexes.workspaceTypeBindings` (scope-independent / global, consulted * unconditionally), and `indexes.namespaceFqnBindings` + * `indexes.namespaceTypeBindings` (per-namespace, consulted only for the * namespaces in `indexes.accessibleNamespacesByScope` for the caller's * module). All but `indexes.bindings` are mutable post-finalize and * populated by hooks; only `indexes.bindings` is frozen. * * `indexes.bindings` is the **finalize-output channel**. After * `finalizeScopeModel` returns, its inner `BindingRef[]` arrays * are deep-frozen by `materializeBindings` and MUST NOT be * mutated by any post-finalize hook. Treat `indexes.bindings` as * immutable from the moment `finalizeScopeModel` returns. * * `indexes.bindingAugmentations` is the **post-finalize * append-only channel**. Hooks like `populateNamespaceSiblings` * append cross-file bindings synthesized after finalize (C# * same-namespace visibility, `using static` member exposure) * into this channel, NOT into `indexes.bindings`. Inner arrays * here are NEVER frozen — hooks `push()` directly. Any consumer * that reads post-finalize workspace bindings MUST query both * index channels via `lookupBindingsAt` * (`scope-resolution/scope/walkers.ts`); the helper returns * finalized refs first, appends unique augmentation refs after, * and dedupes by `def.nodeId` so finalized metadata wins on * duplicate defs. Per-`Scope.bindings` local declarations are the * lexical extraction channel and remain a separate first-tier * lookup for local shadowing. * * `Scope.typeBindings` remains mutable post-finalize per I6 (it * is intentionally not frozen at any point). * * The `ReadonlyMap<...>` types on `ScopeResolutionIndexes` are * compile-time read-guidance for consumers; structural mutation * of `bindingAugmentations` is performed via a deliberate * `as Map<...>` cast inside the hook implementations and is the * ONLY sanctioned channel for post-finalize binding fanout. * * The dev-mode runtime validator * (`validateBindingsImmutability` in * `scope-resolution/validate-bindings-immutability.ts`) surfaces * any drift — i.e. a hook writing to `indexes.bindings` instead * of `bindingAugmentations`, or producing a non-frozen finalized * bucket — via `onWarn` when explicitly enabled by * `NODE_ENV === 'development' || VALIDATE_SEMANTIC_MODEL === '1'` * (`VALIDATE_SEMANTIC_MODEL=0` is an explicit off switch). * * - **I9 — `SemanticModel` is the single authoritative symbol store.** * Every symbol-indexed lookup (key = `nodeId | simpleName | * qualifiedName | filePath`) resolves through * `SemanticModel.{symbols,types,methods,fields}`. Scope-resolution * passes MUST NOT maintain parallel owner-keyed or name-keyed * symbol indexes — `WorkspaceResolutionIndex` is reserved for * `Scope`-valued lookups that `SemanticModel` structurally cannot * carry. * * The `runScopeResolution` orchestrator guarantees this invariant * in two steps: * 1. The legacy `parse` phase populates `SemanticModel` via * `symbolTable.add(...)`. For languages whose extractor * resolves `enclosingClassId` at parse time, class-body defs * are correctly owner-keyed there. * 2. The `reconcileOwnership` pass runs after * `provider.populateOwners(parsed)` and registers any def in * `parsed.localDefs[i]` with a corrected `ownerId` that the * legacy pass missed (primarily Python class-body methods). * Idempotent — duplicates are skipped by `nodeId`. * * Contract for consumers: `model` is `MutableSemanticModel` only * during those two write phases. Downstream passes receive a * narrowed `SemanticModel` (read-only) handle. This is enforced by * `runScopeResolution`'s type-level narrowing at the phase * boundary. * * The dev-mode runtime validator (`validateOwnershipParity`) * surfaces any drift between `parsed.localDefs` ownership and the * registries via `onWarn` when * `NODE_ENV !== 'production' && VALIDATE_SEMANTIC_MODEL !== '0'`. * * This invariant is a **transitional shim**: the architectural * end state is for every language's parse-time extractor to emit * the correct `ownerId` directly, removing the need for * reconciliation. Tracked as a follow-up; see ARCHITECTURE.md § * "Semantic-model source of truth". * * ## Semantic-model source of truth * * `ParsedFile` (from `gitnexus-shared/src/scope-resolution/parsed-file.ts`) * is the single semantic model consumed by both the legacy DAG and the * scope-resolution pipeline. Scope-resolution passes MUST NOT build a * parallel parse representation; if a pass needs AST-level facts that * `ParsedFile` doesn't expose, it should reuse the orchestrator's * `treeCache` (see `RunScopeResolutionInput.treeCache`) rather than * re-invoke `parser.parse(...)` on its own. * * ## Same-graph guarantee * * Edges emitted by `runScopeResolution` and edges emitted by the legacy * DAG are indistinguishable to downstream consumers: * - Node identity: same `generateId(...)` helper, same qualified-name * keyspace, same File/Folder/Method/Class node labels. * - Edge vocabulary: `'import-resolved' | 'global' | 'local-call' | * 'same-file' | 'interface-dispatch' | 'read' | 'write'` — both * paths emit the same reasons (see * `gitnexus/src/core/ingestion/call-processor.ts` for the legacy * emitter and `passes/receiver-bound-calls.ts` / * `passes/free-call-fallback.ts` for the scope-resolution emitters). * - Overload disambiguation: both paths use * `generateId('Method', ...)` suffixed with `parameterTypes` when a * method has overloads — see `graph-bridge/ids.ts`. * * The CI parity workflow (`.github/workflows/ci-scope-parity.yml`) * runs both paths on every migrated language's fixture corpus and * fails if the graph outputs diverge. * * Plan that introduced most of these invariants: * `docs/plans/2026-04-20-001-refactor-emit-pipeline-generalization-plan.md`. */ import type { BindingRef, Callsite, ConstraintContext, ParsedFile, ReferenceSite, ScopeId, SupportedLanguages, SymbolDefinition } from '../../../../_shared/index.js'; import type { KnowledgeGraph } from '../../../graph/types.js'; import type { GraphNodeLookup } from '../graph-bridge/node-lookup.js'; import { LanguageProvider } from '../../language-provider.js'; import { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js'; import type { SemanticModel } from '../../model/semantic-model.js'; import type { ConversionRankFn } from '../passes/overload-narrowing.js'; /** A LinearizeStrategy receives the full ancestor map so C3-style * algorithms (which need to merge each parent's MRO) can implement * themselves. Python's depth-first first-seen only consumes * `directParents` and `parentsByDefId`. */ export type LinearizeStrategy = (classDefId: string, directParents: readonly string[], parentsByDefId: ReadonlyMap) => string[]; /** Result of `ScopeResolver.arityCompatibility` — mirrors `RegistryProviders.arityCompatibility`. */ export type ArityVerdict = 'compatible' | 'unknown' | 'incompatible'; export type ReceiverMemberResolution = { readonly kind: 'resolved'; readonly definition: SymbolDefinition; } | { readonly kind: 'ambiguous'; readonly candidateIds: readonly string[]; }; /** Re-exported for ScopeResolver consumers — same shape as * `RegistryProviders.constraintCompatibility`'s third parameter. */ export type { ConstraintContext } from '../../../../_shared/index.js'; export interface ScopeResolver { /** Identity for telemetry + per-language flag check. */ readonly language: SupportedLanguages; /** Parsing-side hook bag consumed by `extractParsedFile`. The * same `LanguageProvider` reference flows through both interfaces * to keep parsing and emit semantics in sync. */ readonly languageProvider: LanguageProvider; /** Reason text on emitted IMPORTS edges. Mirrors the legacy DAG's * per-language convention so consumers asserting on reason keep * working. */ readonly importEdgeReason: string; /** * Resolve an import statement's `targetRaw` (e.g. `models.user`, * `./helpers`) into an absolute repo-relative file path, or `null` * for unresolvable / external modules. * * Called once per `ParsedImport` during `finalizeScopeModel`. The * Python implementation wraps `resolvePythonImportTarget`. * * `allFilePaths` is the workspace's file set — needed by per-language * resolvers that must distinguish "this module exists in the repo" * from "this module is external" (Python's fallback resolver, for * example). * * `resolutionConfig` is the opaque value returned by * `loadResolutionConfig` (loaded once per workspace pass by the * orchestrator). TypeScript uses this to thread `tsconfig.json` path * aliases through to the standard resolver. Languages that don't * need any extra config ignore the parameter. */ resolveImportTarget(targetRaw: string, fromFile: string, allFilePaths: ReadonlySet, resolutionConfig?: unknown): string | readonly string[] | null; /** * Enumerate names visible through a wildcard import after the target * module scope has been linked. Languages that do not support * wildcard-style imports leave this undefined. */ readonly expandsWildcardTo?: (targetModuleScope: ScopeId, parsedFiles: readonly ParsedFile[]) => readonly string[]; /** * Optional one-shot loader for cross-file import-resolution config * (e.g. tsconfig path aliases for TypeScript, go.mod paths for Go, * composer.json autoload for PHP). The orchestrator calls this once * per workspace pass with the repo root and threads the result into * every subsequent `resolveImportTarget` call as the * `resolutionConfig` parameter. * * Languages that don't need any per-workspace config leave this * undefined; the orchestrator threads `undefined` to * `resolveImportTarget` in that case. Returning `null` is also * supported and equivalent to "no config available". * * May be sync or async — the orchestrator awaits the result. The * shape is opaque to the orchestrator (`unknown`); the per-language * `resolveImportTarget` casts it to the language's expected shape. */ loadResolutionConfig?(repoPath: string): Promise | unknown; /** * Per-scope binding-merge precedence. The shared finalize pass * collects bindings from multiple sources (local declarations, * imports, namespace, wildcard, reexport) and asks the language * how to order them. * * Python uses LEGB: local > import / namespace / reexport > wildcard. */ mergeBindings(existing: readonly BindingRef[], incoming: readonly BindingRef[], scopeId: ScopeId): BindingRef[]; /** * Per-language arity compatibility between a callsite and a * candidate def. The shared `MethodRegistry.lookup` consults this * to penalize incompatible candidates without disqualifying them * outright. Note arg order — `(callsite, def)` matches the * `RegistryProviders` contract; some legacy provider impls use * `(def, callsite)` and need an adapter at the wiring site. */ arityCompatibility(callsite: Callsite, def: SymbolDefinition): ArityVerdict; /** * Per-language constraint compatibility between a callsite and a * candidate `def` that carries `templateConstraints` metadata. * Mirrors `arityCompatibility` semantics: the three-valued verdict * MUST treat `'unknown'` as keep-candidate (monotonicity — adding * a predicate can only narrow correctly, never produce a wrong * edge). Consulted by `narrowOverloadCandidates` after the arity * and parameter-type filters. * * Optional. Languages without constrained-overload semantics * (SFINAE, `requires` clauses, trait bounds, conditional types) * leave this undefined and the constraint filter is a pass-through. * * C++ is the first consumer; see `languages/cpp/constraint-filter.ts` * for the Tier-A predicate registry and Kleene 3-valued evaluator. */ readonly constraintCompatibility?: (callsite: ReferenceSite, def: SymbolDefinition, ctx: ConstraintContext) => ArityVerdict; /** * Compute the method-dispatch order for every Class def in the * workspace. Python uses depth-first first-seen via * `pythonLinearize`; future languages may use C3 (Ruby, Python's * real MRO when we go beyond the simplified walk), single- * inheritance only (Java), or empty-map (languages without * inheritance). */ buildMro(graph: KnowledgeGraph, parsedFiles: readonly ParsedFile[], nodeLookup: GraphNodeLookup): Map; /** * Optional parallel MRO that EXCLUDES mixin-like augmentation (e.g., PHP * traits). Returns the inheritance-only ancestor chain — the same kind * of map as `buildMro` but built only from inheritance edges (EXTENDS). * * Used by the shared super-branch dispatch in `receiver-bound-calls` * so that `parent::method()` walks the inheritance chain only, not the * trait-augmented one. PHP semantics: `parent::` explicitly bypasses * traits, even when a composed trait shadows a same-named parent method. * * Languages without mixin-like semantics leave this undefined — callers * fall back to `buildMro`/`mroFor`, which for those languages is already * the inheritance chain. */ readonly buildExtendsOnlyMro?: (graph: KnowledgeGraph, parsedFiles: readonly ParsedFile[], nodeLookup: GraphNodeLookup) => Map; /** * Optional pre-MRO hook to emit heritage edges (IMPLEMENTS) that the * generic `preEmitInheritanceEdges` pass cannot produce. Runs AFTER * `preEmitInheritanceEdges` (which emits EXTENDS from `@reference.inherits` * sites) and BEFORE `buildMro` (which reads the graph for EXTENDS + * IMPLEMENTS). Languages whose heritage declarations are syntactic method * calls rather than grammar-level heritage clauses (e.g., Ruby * `include`/`extend`/`prepend`) use this hook to emit IMPLEMENTS edges * from parsed import or reference data. * * Receives the graph (writable), parsedFiles, nodeLookup, and the finalized * `ScopeResolutionIndexes` — the same scope/import/def model * `preEmitInheritanceEdges` resolves against, and already a first-class part * of this contract (the structure/binding hooks below take it too), so the * trailing `scopes` parameter is not a new type dependency here. It is * appended and optional so implementations that don't need scope-aware * resolution keep their narrower signature. * * `scopes` has exactly ONE consumer: the Rust resolver — see * `emitRustTraitImplEdges` in languages/rust/scope-resolver.ts — which * resolves `impl T for S` trait/struct names through the scope chain + * import-aware disambiguation (refusing ambiguous matches) instead of a * global last-write-wins simple-name index (#1951). Other implementations * (e.g. Ruby `include`/`extend`/`prepend`) ignore it and keep the 3-arg * shape. Must be idempotent (the orchestrator may call it more than once * during re-resolution). * * Default: undefined (no extra heritage edges needed). */ readonly emitHeritageEdges?: (graph: KnowledgeGraph, parsedFiles: readonly ParsedFile[], nodeLookup: GraphNodeLookup, scopes?: ScopeResolutionIndexes) => void; /** * Optional hook to emit IMPORTS edges that no syntactic import * statement produces. Some languages grant files implicit visibility * of one another within a compilation unit (e.g. every file in a * build target sees its siblings' top-level declarations without an * explicit import). The generic import pipeline only emits File→File * IMPORTS edges from finalized `ImportEdge`s, so a language with this * implicit-visibility rule has no edge to emit through that path. * * Runs immediately after `emitHeritageEdges` (so it shares the same * pre-MRO surface: writable graph, parsedFiles, nodeLookup). Must be * idempotent — the orchestrator may invoke it more than once during * re-resolution. Implementations dedup their own emissions. * * `resolutionConfig` is the opaque per-workspace value returned by * `loadResolutionConfig` (same channel threaded into `resolveImportTarget`). * Swift uses it to group same-module files by the SPM target subtree; * languages that don't need per-workspace config ignore the trailing * parameter (it is optional so existing impls keep compiling). * * Default: undefined (cross-file visibility requires an explicit * import; the finalized-ImportEdge pipeline covers it). */ readonly emitImplicitImportEdges?: (graph: KnowledgeGraph, parsedFiles: readonly ParsedFile[], nodeLookup: GraphNodeLookup, resolutionConfig?: unknown) => void; /** * Restore capture-time per-file side-channel state that `emitScopeCaptures` * produces as a side effect into module-level maps, NOT onto the returned * `ParsedFile`. Such state never crosses the worker boundary: in worker-mode * parses `emitScopeCaptures` runs inside the worker, so its module-level * marks are populated in the WORKER process. The main thread then reuses the * serialized `ParsedFile` (see `RunScopeResolutionInput.preExtractedParsedFiles`) * and skips `extractParsedFile`, so those marks would otherwise be missing in * the main process where resolution consumes them. * * This hook reads the worker-serialized snapshot from * `parsed.captureSideChannel` (produced by the matching * `LanguageProvider.collectCaptureSideChannel` hook in the worker) and writes * it back into the module maps. It does NO tree-sitter parse and needs no * source `content` — that is the whole point of #1983 (the prior re-parse * replay re-introduced the main-thread tree-sitter OOM on huge `.h`/`.cpp` * repos and was replaced by this data-only restore). * * C++ is the only language with this pattern today: `emitCppScopeCaptures` * records ADL call-site arg shapes, inline-/anonymous-namespace ranges, * dependent-base names, and file-local linkage into module maps that * `populateOwners` and the ADL / two-phase-lookup passes read on the main * thread. Without this restore, all of that is empty on the worker path and * advanced C++ resolution (ADL / SFINAE-adjacent / inline-namespace) silently * produces zero edges. * * Called by `runScopeResolution` ONLY for pre-extracted files (the worker * already populated the marks in-process for freshly extracted files, so the * fresh-extract leg never calls this). Runs BEFORE `populateOwners(parsed)` * so the resolved-range Sets it repopulates are visible to that hook. * * Languages whose `emitScopeCaptures` is pure (the contract default — see * `scope-extractor.ts`) leave this undefined; the restore is a no-op for them. * * @param parsed The pre-extracted ParsedFile being reused. Its * `captureSideChannel` carries the worker-computed data. */ readonly applyCaptureSideChannel?: (parsed: ParsedFile) => void; /** * Mutate `parsed.localDefs[i].ownerId` to point at the structural * owner. Python's rule: methods (Function defs whose parent scope * is Class) AND class-body fields (defs in Class scopes) are owned * by the enclosing class. Other languages may have richer rules * (e.g., Java inner-class qualification). */ populateOwners(parsed: ParsedFile): void; /** * Optional workspace-wide ownership reconciliation for languages whose * member owner can be declared in a different file from the owner type. * Runs after every file has had `populateOwners(parsed)` applied, but * still before `reconcileOwnership`, so stamped ownerIds are copied into * the semantic model registries. */ readonly populateWorkspaceOwners?: (parsedFiles: readonly ParsedFile[], ctx: { readonly fileContents: ReadonlyMap; }) => void; /** * Recognize a `super(...)`-style receiver text. Python returns * `/^super\s*\(/.test(t)`. Java returns `t === 'super'`. C++ may * also need `this` capture. Languages without inheritance return * constant `false`. * * For languages where the answer depends on caller context (e.g. * C++, where `Base::method()` is a super call ONLY when `Base` is * actually a base of the caller's enclosing class, and namespace- * qualified calls like `Singleton::getInstance()` must NOT be * misclassified), implement the optional `isSuperReceiverInContext` * variant below. The receiver-bound-calls pass prefers the context- * aware variant when both are defined. */ isSuperReceiver(receiverText: string): boolean; /** * Optional context-aware variant of `isSuperReceiver`. When defined, * the receiver-bound-calls pass prefers this hook over the simple * `isSuperReceiver(text)` form. Languages where super classification * is purely text-driven (Python, Java, PHP) omit this hook and the * simple form is used unchanged. * * C++ uses this to distinguish `Base::method()` (super call when * `Base` is in the caller's MRO) from `Singleton::getInstance()` * (ordinary namespace-qualified call). Without this, the regex * heuristic `/^[A-Z]\w*::/` misclassifies any uppercase-qualified * call as a super-receiver call and routes it through the wrong * resolution branch. * * Returns `true` ONLY when: * - the receiver text parses as `::<...>` (or another super- * form the language recognizes), AND * - `` resolves (via scope chain) to a class-like def, AND * - that class is in the MRO of the caller's enclosing class. * * Returns `false` for namespace-qualified calls, unresolved names, * class-qualified calls where the class is NOT in the caller's MRO, * and any text the simple `isSuperReceiver` hook also rejects. */ readonly isSuperReceiverInContext?: (receiverText: string, callerScope: ScopeId, scopes: ScopeResolutionIndexes) => boolean; /** * Whether the orchestrator should run `propagateImportedReturnTypes` * after finalize. Default `true`. TypeScript with explicit type * exports may want a different propagation strategy and opt out. */ readonly propagatesReturnTypesAcrossImports?: boolean; /** * Whether the compound-receiver resolver should fall back to * walking field types when method lookup on the receiver's class * fails (the "Phase-9C unified fixpoint" heuristic). Default * `true`. Strictly-typed languages should set `false` because the * heuristic can produce edges that wouldn't survive a real type * check. */ readonly fieldFallbackOnMethodLookup?: boolean; /** * Unwrap a property-style collection accessor on a typed receiver * to its element type. Called by `resolveCompoundReceiverClass` * when walking dotted member-access chains of the form * `receiver.Accessor`. The provider returns the element type's * simple name, or `undefined` when the accessor doesn't unwrap — * in which case the regular field-walk resumes. * * Use this only for languages that expose collection views as * properties rather than method calls; languages whose collection * views are `.values()` / `.keys()` method calls leave this * undefined and let the normal call-expression branch handle them. */ readonly unwrapCollectionAccessor?: (receiverType: string, accessor: string) => string | undefined; /** * Collapse member-call CALLS edges by `(caller, target)` rather * than per-site. Default `false` — scope-resolution's contract * invariant is per-site dedup. * * Enable this when the language's graph convention is one edge per * caller/target pair regardless of how many syntactic sites exist, * e.g. to match a legacy graph's edge count so downstream * consumers don't see a migration-induced inflation. */ readonly collapseMemberCallsByCallerTarget?: boolean; /** * Allow free-call emission to fall back to a unique workspace-wide * callable match when lexical/import bindings miss. Kept opt-in * because this mirrors legacy resolver behavior for some languages * but is too loose as a default for strict module systems. */ readonly allowGlobalFreeCallFallback?: boolean; /** * When true, a constructor-form call `Type(...)` links to the Class def * itself rather than its explicit Constructor def. Default * (undefined/false) targets the explicit Constructor when one exists, * else falls back to the Class. Languages whose call graph models * `Type(...)` as a reference to the type (not its initializer) — e.g. * Swift — opt in. */ readonly constructorCallTargetsClass?: boolean; /** * Optional per-slot conversion-rank function for overload resolution. * When provided, `narrowOverloadCandidates` uses ranked scoring as a * fallback when the exact-type filter produces no match. The function * returns a numeric cost (0 = exact, 1 = promotion, 2 = standard * conversion, Infinity = incompatible) for converting an argument * type to a parameter type. * * The conversion-rank table is language-specific (issue #1578 pitfall: * keep it out of shared overload-narrowing). C++ provides * `cppConversionRank`; other languages define their own if needed. */ readonly conversionRankFn?: ConversionRankFn; /** * Optional predicate to identify definitions with file-local linkage * (e.g. C `static` functions). When provided, `pickUniqueGlobalCallable` * excludes defs where `isFileLocalDef(def) === true` and the def lives * in a different file from the caller. This prevents the global free-call * fallback from creating CALLS edges to file-local symbols that are * logically invisible from the caller's translation unit. * * Languages without file-local linkage semantics leave this undefined. */ readonly isFileLocalDef?: (def: SymbolDefinition) => boolean; /** * Optional predicate to identify members for which dispatch through * an instance receiver is **invalid at the language level** — i.e. * calling `instance.member()` would be a compile error or a * type-system violation, even if a member of that name exists on * the receiver's class. When provided, the receiver-bound calls * pass filters out such members at every instance-receiver dispatch * case (Case 0 compound receiver, Case 3b chain-typebinding, Case 4 * simple typeBinding, Case 5 value-receiver bridge) so the resolver * does not emit a misleading `CALLS` edge for a call site the * language itself would reject. * * **Reserved for the "instance receiver is invalid" semantic only.** * Hooks for languages where static / class-level members are still * legally callable through an instance (Python `@staticmethod`, * JavaScript `static` methods accessed via the prototype chain in * some lookup paths) should return `false` for those members — the * filter would silently suppress legitimate edges otherwise. The * canonical fit today is Kotlin companion-object methods, where * `instance.companionMethod()` is a compile error. * * Case 2 (class-name receiver) is intentionally unaffected: a call * through the class name (`Foo.staticMethod()`) is a legitimate * dispatch. * * Case 0.5 (implicit `this` receiver) currently fires only for * languages with `resolveThisViaEnclosingClass === true` (C++ at * time of writing), none of which expose static-only semantics. A * future language that enables BOTH `resolveThisViaEnclosingClass` * AND `isStaticOnly` must wire the filter into Case 0.5's chain * walk too — see the inline note in `receiver-bound-calls.ts`. * * Languages without static-only semantics leave this undefined and * the legacy unfiltered behavior applies (every owned member of the * receiver class is a dispatch candidate). */ readonly isStaticOnly?: (def: SymbolDefinition) => boolean; /** * Optional predicate to gate free-call fallback emission by caller-side * visibility. When provided, `pickUniqueGlobalCallable` rejects candidates * the caller cannot legally reach — e.g., a PHP function in a different * namespace with no `use function` import, which PHP runtime would treat * as `Call to undefined function`. Returning `false` blocks the candidate; * returning `true` allows it; undefined-default keeps current behavior * (no visibility filtering, equivalent to "all candidates visible"). * * The hook receives the caller's `ParsedFile` (so it can consult * `parsedImports`, `moduleScope`, etc.) and the candidate `SymbolDefinition`. * The predicate must be pure: same inputs → same answer. * * Languages without namespace-scoped function resolution leave this undefined. */ readonly isCallableVisibleFromCaller?: (ctx: { readonly callerParsed: ParsedFile; readonly candidate: SymbolDefinition; /** Caller's enclosing scope id. Languages that gate visibility on * caller scope (e.g. C++ two-phase template lookup) consult it; * others ignore. Optional so existing implementations stay valid. */ readonly callerScope?: ScopeId; /** ScopeResolutionIndexes for scope-tree walks. Optional for the * same reason as `callerScope`. */ readonly scopes?: ScopeResolutionIndexes; }) => boolean; /** * Optional argument-dependent-lookup (ADL / Koenig lookup) hook for * languages with C++-style associated-namespace candidate addition. * * Runs in the free-call fallback alongside ordinary unqualified lookup. * The fallback merges ordinary candidates with ADL candidates and applies * overload narrowing over the union. * * The hook inspects the call site's argument types, computes the * associated namespace set, and returns either: * - an array of candidate `SymbolDefinition`s to add to the * ordinary-lookup candidate pool. * - `undefined` when ADL contributes no candidates. * * Languages without C++-style ADL leave this undefined. The * cross-language contract is "additive tier" — defining the hook never * removes candidates the prior tier would have produced. */ readonly resolveAdlCandidates?: (site: { readonly name: string; readonly arity?: number; readonly argumentTypes?: readonly string[]; readonly atRange: { readonly startLine: number; readonly startCol: number; }; }, callerParsed: ParsedFile, scopes: ScopeResolutionIndexes, parsedFiles: readonly ParsedFile[]) => readonly SymbolDefinition[] | undefined; /** * Optional resolver for qualified-receiver member calls where the * receiver is a namespace (not a class) and ordinary scope-chain / * import resolution doesn't find the member. C++ uses this for * `outer::foo()` style calls and to walk through inline-namespace * children transitively (`outer::v1::foo` reachable as `outer::foo`). * * Languages whose qualified-name semantics are already covered by the * receiver-bound-calls Case-1 namespace-targets path (e.g., Python's * `import X; X.foo()`) leave this undefined. * * Receiver-bound-calls invokes this hook AFTER Case 1 (namespace * imports) and AFTER Case 2 (class-name receiver) fail to resolve. * Returns the target def, `'ambiguous'` when multiple inline-namespace * children declare the same name (suppresses edge emission), or * `undefined` to fall through to the remaining cases. */ readonly resolveQualifiedReceiverMember?: (receiverName: string, memberName: string, callerScope: ScopeId, scopes: ScopeResolutionIndexes, parsedFiles: readonly ParsedFile[], callsite?: Callsite) => SymbolDefinition | 'ambiguous' | undefined; /** * Optional language-specific member-lattice lookup. Runs for a resolved * simple receiver type before the generic flattened-MRO walk. Languages * with lookup-set semantics that cannot be represented by one linear MRO * may resolve a member, report ambiguity (which suppresses fallback), or * return undefined to retain the shared behavior. */ readonly resolveReceiverMember?: (ownerDef: SymbolDefinition, memberName: string, callsite: Callsite, scopes: ScopeResolutionIndexes, model: SemanticModel) => ReceiverMemberResolution | undefined; /** * Enable the receiver-bound Case 0.5 fallback for explicit `this` * receivers (`this->m()` / `this.m()`) that resolves against the * enclosing class + MRO even when no explicit `this` typeBinding is * present in scope. * * Keep disabled for languages where the existing type-binding path * (Case 4) already handles `this` correctly and overload ambiguity * suppression must remain unchanged. */ readonly resolveThisViaEnclosingClass?: boolean; /** * Optional post-finalize hook to inject cross-file bindings that * aren't modeled via explicit imports. Runs after * `buildWorkspaceResolutionIndex` and before * `propagateImportedReturnTypes`. * * Use this for languages where a compiler-implicit visibility rule * makes names visible across files without a syntactic import — * for example a shared-namespace convention where types declared * in the same namespace see each other without a `using` / `import` * statement. Languages that require explicit imports for cross-file * visibility leave this undefined. */ readonly populateNamespaceSiblings?: (parsedFiles: readonly ParsedFile[], indexes: ScopeResolutionIndexes, ctx: { readonly fileContents: ReadonlyMap; /** Pre-parsed tree-sitter trees keyed by file path. Same cache * the orchestrator hands to `extractParsedFile`; passing it * through here lets per-language hooks read the AST without * triggering a second parse. Cache miss = the hook re-parses * itself; the cache is opt-in for hooks that need AST-level * facts beyond what `ParsedFile` exposes. */ readonly treeCache?: { get(filePath: string): unknown; }; /** Opaque per-workspace value from `loadResolutionConfig` (same * channel threaded into `resolveImportTarget`). Swift uses it to * group same-module siblings by the SPM target subtree; languages * that don't need per-workspace config ignore it. Optional so * existing impls keep compiling. */ readonly resolutionConfig?: unknown; }) => void; /** * Whether the compound-receiver resolver should walk up from a * class scope to ancestor (Module) scopes when looking up a * method's return-type typeBinding. Default `false`. * * Set `true` only when the provider stores method return-type * bindings on the enclosing Module scope rather than on the class * scope. Without this walk-up, chain resolution fails for methods * whose return types were hoisted to module scope. * * Providers that attach return-type bindings directly to the class * scope leave this undefined — enabling the walk-up for them would * add an unnecessary branch and risk picking up unrelated module- * level bindings. */ readonly hoistTypeBindingsToModule?: boolean; /** * Optional: detect structural (duck-typing) interface implementations. * Languages like Go use structural typing — a struct satisfies an * interface if its method set is a superset, without an explicit * `implements` keyword. Runs after finalize, before resolution passes. * Returns: Map. * Default: undefined (no structural interface detection). */ readonly detectInterfaceImplementations?: (parsedFiles: readonly ParsedFile[], indexes: ScopeResolutionIndexes, model: SemanticModel) => Map; /** * Optional: mirror typeBindings from namespace-import target modules * into the importer's module scope. Languages like Go use namespace * imports (`import "pkg"`) and need the target package's exported * typeBindings visible in the importer's scope chain for cross-package * return-type resolution (e.g. `x := pkg.NewUser(); x.Save()` needs * `NewUser → User` mirrored from the target package). Runs after * `populateNamespaceSiblings` and before `propagateImportedReturnTypes` * so the SCC-ordered pass sees the mirrored bindings. * * `resolutionConfig` is the opaque per-workspace value returned by * `loadResolutionConfig` (same channel threaded into `resolveImportTarget`). * Swift uses it to group same-module sibling files by the SPM target * subtree; languages that don't need per-workspace config ignore the * trailing parameter (it is optional so existing impls keep compiling). * * Default: undefined (no namespace typeBinding mirroring). */ readonly mirrorNamespaceTypeBindings?: (parsedFiles: readonly ParsedFile[], indexes: ScopeResolutionIndexes, workspaceIndex: import('../../scope-resolution/workspace-index.js').WorkspaceResolutionIndex, resolutionConfig?: unknown) => void; /** * Optional: bind for-range loop variables to their element/value types. * Languages like Go need to resolve `for _, v := range m` where `m` is * `map[K]V` — the variable `v` should bind to `V`. Runs after finalize, * before resolution passes. Mutates scope typeBindings via the Map cast * convention (see Invariant I8). * Default: undefined (no range variable binding). */ readonly populateRangeBindings?: (parsedFiles: readonly ParsedFile[], indexes: ScopeResolutionIndexes, ctx: { readonly fileContents: ReadonlyMap; readonly treeCache?: { get(filePath: string): unknown; }; }) => void; /** * Optional hook to expand the set of file paths handed to the scope- * resolution run for this language. * * Called once per language with: * - `primaryFilePaths` — files whose `getLanguageFromFilename` === this * resolver's `language` (e.g. all `.vue` files). * - `preExtractedByPath` — ParsedFile cache from the parse phase. * - `entryFileContents` — raw source text of the primary files. * - `allScannedPaths` — complete set of paths in the repository. * - `resolutionConfig` — language-specific config (tsconfig paths, …). * * Return value: the full set of paths to include in the scope-resolution * run. May be a superset of `primaryFilePaths`. * * Vue uses this hook to collect the transitive TS/JS import closure of * every `.vue` file so that cross-file imports (`import { fn } from './api'`) * resolve correctly within a single Vue scope-resolution pass. * * This hook keeps language-specific scope-context policy inside the language * module, preventing shared pipeline code (`phase.ts`) from naming individual * languages. * * Default: undefined (use only `primaryFilePaths`). */ readonly collectScopeContextPaths?: (options: { readonly primaryFilePaths: readonly string[]; readonly preExtractedByPath: ReadonlyMap; readonly entryFileContents: ReadonlyMap; readonly allScannedPaths: ReadonlySet; readonly resolutionConfig: unknown; }) => Set; /** * Optional post-resolution hook for emitting language-specific graph edges * that cannot be derived from scope captures or import resolution alone. * * Runs AFTER all standard edge-emission passes (receiver-bound CALLS, * free-call fallback, references-via-lookup, and import edges). Receives * the fully-resolved graph, all ParsedFiles, the node lookup, the finalized * scope indexes, and the raw file-content map. * * Vue uses this hook to emit: * - `CALLS` (`vue-template-component`) for PascalCase component elements * - `BINDS_EVENT_HANDLER` for `@event="handler"` on component elements * - `EMITS_EVENT` for `emit('eventName', …)` calls in script blocks * - `ACCESSES` (`vue-template-attribute`) for `:prop="var"` bindings * * Unlike `emitImplicitImportEdges` and `emitHeritageEdges` (which run * before MRO construction), this hook runs last, after the full graph is * populated, so it can safely query node existence and resolved import * targets via `indexes.imports`. * * Default: undefined (no supplementary edges needed). */ readonly emitPostResolutionEdges?: (graph: KnowledgeGraph, parsedFiles: readonly ParsedFile[], nodeLookup: GraphNodeLookup, indexes: ScopeResolutionIndexes, ctx: { readonly fileContents: ReadonlyMap; readonly resolutionConfig?: unknown; }) => void; /** * Optional post-resolution pass: emit CALLS edges for member-call sites * whose receiver cannot be typed by the scope chain (no `TypeRef`). * Dynamically-typed languages with untyped/`mixed`/`Any` parameters use * this hook to recover the call edge via workspace-wide method-name * lookup, mirroring what their legacy resolvers did. * * Runs AFTER `emitReceiverBoundCalls` and BEFORE `emitFreeCallFallback`. * Implementations MUST: * - Skip sites already in `handledSites` (Invariant I2). * - Add resolved site keys to `handledSites` before returning. * - Stay narrow: a unique workspace-wide match is the safe baseline. * Multi-candidate fallbacks should narrow by arity / argument types * before emitting to keep false-positive rate bounded. * * Returns the number of edges emitted (for telemetry). * * Default: undefined (no unresolved-receiver fallback). */ readonly emitUnresolvedReceiverEdges?: (graph: KnowledgeGraph, scopes: ScopeResolutionIndexes, parsedFiles: readonly ParsedFile[], nodeLookup: GraphNodeLookup, handledSites: Set, model: SemanticModel) => number; }