<aside> πŸ“Œ

This RFC is the scope-graph continuation of the Fuzzy Lookup Elimination RFC and implements the resolution-layer side of Ingestion Roadmap Phase 3 (Wire SemanticModel). It is triggered by PR #902 review feedback: "resolution must use the semantic model with O(1) lookups β€” that is the sole purpose of the semantic model."

</aside>

Property Value
Type refactor (architecture)
Status active β€” design locked, awaiting community review
Date 2026-04-18
Predecessor RFC: From fuzzy lookups to a semantic model - language-agnostic resolution roadmap
Complements πŸ—ΊοΈ Ingestion Roadmap β€” Phase 3 (Wire SemanticModel) + Phase 4 (Import strategies)
Triggering feedback GitNexus#902 β€” three CHANGES_REQUESTED reviews on the extraction/resolution phase split
Related issues #429 centralize confidence constants Β· #884 gitnexus-shared consolidation
Live tracking Meta issue #909 β€” 39 child tickets (Ring 1–5), progress checkboxes, dependency graph

TL;DR

GitNexus already has a SemanticModel (SymbolTable + Type/Method/Field registries + HeritageMap) that resolves calls through a 5-stage DAG inside call-processor.ts. The DAG still reads AST fragments at resolution time and still uses a tiered TieredCandidates API where the caller has to reason about where a match was found. This RFC locks a design that:

  1. Makes the scope tree the canonical visibility spine of the SemanticModel (containers + block + expression scopes, full granularity).
  2. Replaces ResolutionContext.resolve(name, file) + TieredCandidates with three scope-aware per-kind entry points: ClassRegistry.lookup(name, scope), MethodRegistry.lookup(name, scope), FieldRegistry.lookup(name, scope) β€” all returning Resolution[] with { def, confidence, evidence }.
  3. Collapses the call-resolution DAG into a single Registry.lookup(name, scope) call. TypeEnv becomes Scope.typeBindings; implicit-receiver inference becomes a strict resolveTypeRef path; dispatch-strategy selection becomes caller-side registry selection.
  4. Keeps SymbolTable and TypeRegistry as internal contributors β€” the resolver never calls them directly.
  5. Rolls out shadow-first: per-language PRs run both the legacy DAG and the new registries, diff results into a parity dashboard, and flip REGISTRY_PRIMARY_<LANG> when β‰₯99% fixture + β‰₯98% corpus parity is reached. DAG retirement only when all production-classified languages are flipped and stable for a release cycle.

The design preserves every public invariant of the knowledge graph: no changes to node/edge schema, no changes to MCP contracts, no changes to graph output. All delta lives inside the ingestion pipeline.


Β§1 β€” Architecture Overview

Two walls, one spine.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  AST (tree-sitter    β”‚                          β”‚  Knowledge graph     β”‚
β”‚  + COBOL regex)      β”‚                          β”‚  (LadybugDB)         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–²β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚                                                 β”‚
           β”‚ extraction                  resolution          β”‚
           β”‚ (AST-reading)               (model-only)        β”‚
           β–Ό                                                 β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                      Semantic Model                                   β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚  β”‚  ScopeTree (spine)                                           β”‚    β”‚
β”‚  β”‚    ─ Scope nodes: {bindings, ownedDefs, imports,             β”‚    β”‚
β”‚  β”‚                    typeBindings, range, parent}              β”‚    β”‚
β”‚  β”‚    ─ PositionIndex (O(log N) scope-at-position)              β”‚    β”‚
β”‚  β”‚    ─ Parent pointers (O(D) scope-chain walk)                 β”‚    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚  β”‚  Public API (resolver calls these; nothing else)             β”‚    β”‚
β”‚  β”‚    ClassRegistry.lookup(name, scope)  β†’ Resolution[]         β”‚    β”‚
β”‚  β”‚    MethodRegistry.lookup(name, scope) β†’ Resolution[]         β”‚    β”‚
β”‚  β”‚    FieldRegistry.lookup(name, scope)  β†’ Resolution[]         β”‚    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚  β”‚  Internal contributors (feed into lookups; never called      β”‚    β”‚
β”‚  β”‚  directly by the resolver)                                   β”‚    β”‚
β”‚  β”‚    SymbolTable Β· TypeRegistry                                β”‚    β”‚
β”‚  β”‚    DefIndex Β· ModuleScopeIndex Β· QualifiedNameIndex          β”‚    β”‚
β”‚  β”‚    MethodDispatchIndex Β· ExportMap Β· ImportGraph             β”‚    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Wall 1 β€” Extraction β†’ SemanticModel. Only extraction code reads the AST. It produces: scope tree, declarations attached to scopes, raw import edges on module-level scopes, type bindings, reference sites. No resolution happens here.

Wall 2 β€” SemanticModel β†’ Resolver. Only the resolver calls Registry.lookup(name, scope). It never sees the AST. It emits one edge to the knowledge graph per call, tagged with confidence + evidence.

This matches the PR #902 review requirements verbatim: extraction is the sole AST-reader; resolution is the sole model-reader; the two phases talk through one well-typed spine and three well-typed queries.


Β§2 β€” Data Model (LOCKED β€” authoritative source)