fix: format README important-notice table and clean up fuzzy stats

- Add missing space before URL in README comparison table so the link
  renders correctly in all Markdown renderers.
- Remove fuzzyCallCount / fuzzyCallableCallCount tracking that was
  added for short-term debugging: delete the counters from SymbolTable,
  ResolutionContext and the related pipeline console.log, and update
  the affected unit-test expectations.
- Adjust methodByOwner indexing to exclude Constructor entries, and
  update the corresponding unit test assertion.

Signed-off-by: Cocoon-Break <54054995+kuishou68@users.noreply.github.com>
This commit is contained in:
Cocoon-Break 2026-04-22 02:47:25 +00:00
parent 962f22482b
commit bc49ae4494
5 changed files with 2710 additions and 2490 deletions

152
README.md
View file

@ -9,7 +9,7 @@
<h2>Join the official Discord to discuss ideas, issues etc!</h2>
<a href="https://discord.gg/MgJrmsqr62">
<a href="https://discord.gg/AAsRVT6fGb">
<img src="https://img.shields.io/discord/1477255801545429032?color=5865F2&logo=discord&logoColor=white" alt="Discord"/>
</a>
<a href="https://www.npmjs.com/package/gitnexus">
@ -194,7 +194,6 @@ gitnexus analyze --force # Force full re-index
gitnexus analyze --skills # Generate repo-specific skill files from detected communities
gitnexus analyze --skip-embeddings # Skip embedding generation (faster)
gitnexus analyze --skip-agents-md # Preserve custom AGENTS.md/CLAUDE.md gitnexus section edits
gitnexus analyze --skip-git # Index folders that are not Git repositories
gitnexus analyze --embeddings # Enable embedding generation (slower, better search)
gitnexus analyze --verbose # Log skipped files when parsers are unavailable
gitnexus mcp # Start MCP server (stdio) — serves all indexed repos
@ -336,155 +335,6 @@ cd ../gitnexus-web && npm install
npm run dev
```
## Docker
The official Docker setup ships **two signed images** orchestrated by `docker-compose.yaml`:
| Image | Purpose |
| -------------------------------------------------- | ---------------------------------------------------------------------- |
| `ghcr.io/abhigyanpatwari/gitnexus:latest` | CLI / `gitnexus serve` backend (HTTP API on port `4747`, MCP, indexer) |
| `ghcr.io/abhigyanpatwari/gitnexus-web:latest` | Static web UI (port `4173`) |
> **Heads-up — image rename.** Earlier releases published the web UI under
> `ghcr.io/abhigyanpatwari/gitnexus`. Starting with the introduction of the
> bundled backend, that slug now hosts the CLI/server image and the UI moved
> to `ghcr.io/abhigyanpatwari/gitnexus-web`. The previous tags remain
> available for pulling, but new versions are only published under the new
> slugs. Update your `docker run` / compose files accordingly (or just adopt
> the bundled compose).
### One-command setup
```bash
docker compose up -d
```
This starts the server on `http://localhost:4747` and the web UI on
`http://localhost:4173`. The UI auto-detects the server because the browser
runs on the host and reaches the container via the mapped port.
A named volume (`gitnexus-data`) persists the global registry, indexes, and
cloned repos at `/data/gitnexus` inside the server container. To make repos on
your host machine indexable, set `WORKSPACE_DIR` before bringing the stack up:
```bash
WORKSPACE_DIR=$HOME/code docker compose up -d
# Inside the server container the directory is mounted read-only at /workspace.
docker compose exec gitnexus-server gitnexus index /workspace/my-repo
```
### Direct `docker run`
```bash
# Server
docker run --rm -d \
--name gitnexus-server \
-p 4747:4747 \
-v gitnexus-data:/data/gitnexus \
ghcr.io/abhigyanpatwari/gitnexus:latest
# Web UI
docker run --rm -d \
--name gitnexus-web \
-p 4173:4173 \
ghcr.io/abhigyanpatwari/gitnexus-web:latest
```
Optional env file (override image tags, container names, ports, workspace dir):
```bash
cp .env.example .env
docker compose --env-file .env up -d
```
### Versioning & supply-chain protection
The Docker images are version-locked to the npm package:
- Stable images are **only published from `vX.Y.Z` git tags** (via `docker.yml`
triggered directly by the tag push), and the workflow refuses to build unless
the tag exactly matches `gitnexus/package.json`'s version. So
`ghcr.io/abhigyanpatwari/gitnexus:1.6.2` is byte-for-byte the same release
as `npm install gitnexus@1.6.2` — no drift, no floating builds from `main`.
- Release-candidate images (e.g. `:1.7.0-rc.1`) are published alongside each
RC npm release. They are built by `release-candidate.yml` calling `docker.yml`
as a reusable workflow after the RC tag is created and pushed.
- `:latest` is auto-promoted only from non-prerelease tags by the Docker
metadata action, so it always points at a real, npm-published version.
Both images are signed with [Cosign keyless signing][cosign-keyless] using the
workflow's GitHub OIDC identity, and shipped with build provenance and SBOM
attestations. **This is your protection against supply-chain attacks**: even if
an attacker republishes a same-named image elsewhere (or somehow pushes to a
typo-squatted registry), they cannot forge a Cosign signature tied to
`abhigyanpatwari/GitNexus`'s `docker.yml`. Always verify before pulling into
sensitive environments:
**Stable releases** — signed from the `v*` tag ref:
```bash
cosign verify ghcr.io/abhigyanpatwari/gitnexus:1.6.2 \
--certificate-identity-regexp '^https://github\.com/abhigyanpatwari/GitNexus/\.github/workflows/docker\.yml@refs/tags/v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
```
The regex pins the certificate identity to this repo's `docker.yml` workflow
**run from a `v*` tag** — rejecting unsigned images, images signed by other
workflows, and images signed from unprotected refs.
**Release candidates** — signed from `refs/heads/main` (the caller's ref when
`release-candidate.yml` invokes `docker.yml` as a reusable workflow):
```bash
cosign verify ghcr.io/abhigyanpatwari/gitnexus:1.7.0-rc.1 \
--certificate-identity 'https://github.com/abhigyanpatwari/GitNexus/.github/workflows/docker.yml@refs/heads/main' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
```
You can also inspect the build provenance and SBOM:
```bash
cosign download attestation ghcr.io/abhigyanpatwari/gitnexus:1.6.2 \
--predicate-type https://slsa.dev/provenance/v1
```
#### Kubernetes: enforce signatures at admission
For Kubernetes deployments, ship the bundled
[`ClusterImagePolicy`](deploy/kubernetes/cluster-image-policy.yaml) so the
[Sigstore policy-controller][policy-controller] rejects any GitNexus pod whose
image is not signed by this repo's `docker.yml` running from a `vX.Y.Z` tag —
the same identity the `cosign verify` snippet above pins.
```bash
# 1. Install the controller (one-time, cluster-wide)
helm repo add sigstore https://sigstore.github.io/helm-charts && helm repo update
helm install policy-controller -n cosign-system --create-namespace \
sigstore/policy-controller
# 2. Opt your namespace in
kubectl label namespace <your-ns> policy.sigstore.dev/include=true
# 3. Apply the policy
kubectl apply -f deploy/kubernetes/cluster-image-policy.yaml
```
After this, attempting to deploy an unsigned image — or one signed by anything
other than `abhigyanpatwari/GitNexus`'s `docker.yml` at a `v*` tag — fails the
admission webhook before a pod is ever created. This turns the verifiable
signature into an enforced policy, which is the supply-chain control most
clusters actually need.
[cosign-keyless]: https://docs.sigstore.dev/cosign/signing/overview/
[policy-controller]: https://docs.sigstore.dev/policy-controller/overview/
### Files
- [Dockerfile.web](Dockerfile.web) — builds `gitnexus-shared` and `gitnexus-web`, then serves the production frontend.
- [Dockerfile.cli](Dockerfile.cli) — builds the CLI/server (with its native deps) and runs `gitnexus serve --host 0.0.0.0`.
- [docker-compose.yaml](docker-compose.yaml) — starts both signed images side by side.
- [.env.example](.env.example) — overrides for image names, container names, ports, and the workspace mount.
The web UI uses the same indexing pipeline as the CLI but runs entirely in WebAssembly (Tree-sitter WASM, LadybugDB WASM, in-browser embeddings). It's great for quick exploration but limited by browser memory for larger repos.
**Local Backend Mode:** Run `gitnexus serve` and open the web UI locally — it auto-detects the server and shows all your indexed repos, with full AI chat support. No need to re-upload or re-index. The agent's tools (Cypher queries, search, code navigation) route through the backend HTTP API automatically.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,205 @@
/**
* Resolution Context
*
* Single implementation of tiered name resolution. Replaces the duplicated
* tier-selection logic previously split between symbol-resolver.ts and
* call-processor.ts.
*
* Resolution tiers (highest confidence first):
* 1. Same file (lookupExactFull — authoritative)
* 2a-named. Named binding chain (walkBindingChain via NamedImportMap)
* 2a. Import-scoped (lookupFuzzy filtered by ImportMap)
* 2b. Package-scoped (lookupFuzzy filtered by PackageMap)
* 3. Global (all candidates — consumers must check candidate count)
*/
import type { SymbolTable, SymbolDefinition } from './symbol-table.js';
import { createSymbolTable } from './symbol-table.js';
import type { NamedImportBinding } from './import-processor.js';
import { isFileInPackageDir } from './import-processor.js';
import { walkBindingChain } from './named-binding-processor.js';
/** Resolution tier for tracking, logging, and test assertions. */
export type ResolutionTier = 'same-file' | 'import-scoped' | 'global';
/** Tier-selected candidates with metadata. */
export interface TieredCandidates {
readonly candidates: readonly SymbolDefinition[];
readonly tier: ResolutionTier;
}
/** Confidence scores per resolution tier. */
export const TIER_CONFIDENCE: Record<ResolutionTier, number> = {
'same-file': 0.95,
'import-scoped': 0.9,
global: 0.5,
};
// --- Map types ---
export type ImportMap = Map<string, Set<string>>;
export type PackageMap = Map<string, Set<string>>;
export type NamedImportMap = Map<string, Map<string, NamedImportBinding>>;
/** Maps callerFile → (moduleAlias → sourceFilePath) for Python namespace imports.
* e.g. `import models` in app.py → moduleAliasMap.get('app.py')?.get('models') === 'models.py' */
export type ModuleAliasMap = Map<string, Map<string, string>>;
export interface ResolutionContext {
/**
* The only resolution API. Returns all candidates at the winning tier.
*
* Tier 3 ('global') returns ALL candidates regardless of count —
* consumers must check candidates.length and refuse ambiguous matches.
*/
resolve(name: string, fromFile: string): TieredCandidates | null;
// --- Data access (for pipeline wiring, not resolution) ---
/** Symbol table — used by parsing-processor to populate symbols. */
readonly symbols: SymbolTable;
/** Raw maps — used by import-processor to populate import data. */
readonly importMap: ImportMap;
readonly packageMap: PackageMap;
readonly namedImportMap: NamedImportMap;
/** Module-alias map for Python namespace imports: callerFile → (alias → sourceFile). */
readonly moduleAliasMap: ModuleAliasMap;
// --- Per-file cache lifecycle ---
enableCache(filePath: string): void;
clearCache(): void;
// --- Operational ---
getStats(): {
fileCount: number;
globalSymbolCount: number;
cacheHits: number;
cacheMisses: number;
};
clear(): void;
}
export const createResolutionContext = (): ResolutionContext => {
const symbols = createSymbolTable();
const importMap: ImportMap = new Map();
const packageMap: PackageMap = new Map();
const namedImportMap: NamedImportMap = new Map();
const moduleAliasMap: ModuleAliasMap = new Map();
// Per-file cache state
let cacheFile: string | null = null;
let cache: Map<string, TieredCandidates | null> | null = null;
let cacheHits = 0;
let cacheMisses = 0;
// --- Core resolution (single implementation of tier logic) ---
const resolveUncached = (name: string, fromFile: string): TieredCandidates | null => {
// Tier 1: Same file — authoritative match (returns all overloads)
const localDefs = symbols.lookupExactAll(fromFile, name);
if (localDefs.length > 0) {
return { candidates: localDefs, tier: 'same-file' };
}
// Get all global definitions for subsequent tiers
const allDefs = symbols.lookupFuzzy(name);
// Tier 2a-named: Check named bindings BEFORE empty-allDefs early return
// because aliased imports mean lookupFuzzy('U') returns empty but we
// can resolve via the exported name.
const chainResult = walkBindingChain(name, fromFile, symbols, namedImportMap, allDefs);
if (chainResult && chainResult.length > 0) {
return { candidates: chainResult, tier: 'import-scoped' };
}
if (allDefs.length === 0) return null;
// Tier 2a: Import-scoped — definition in a file imported by fromFile
const importedFiles = importMap.get(fromFile);
if (importedFiles) {
const importedDefs = allDefs.filter((def) => importedFiles.has(def.filePath));
if (importedDefs.length > 0) {
return { candidates: importedDefs, tier: 'import-scoped' };
}
}
// Tier 2b: Package-scoped — definition in a package dir imported by fromFile
const importedPackages = packageMap.get(fromFile);
if (importedPackages) {
const packageDefs = allDefs.filter((def) => {
for (const dirSuffix of importedPackages) {
if (isFileInPackageDir(def.filePath, dirSuffix)) return true;
}
return false;
});
if (packageDefs.length > 0) {
return { candidates: packageDefs, tier: 'import-scoped' };
}
}
// Tier 3: Global — pass all candidates through.
// Consumers must check candidate count and refuse ambiguous matches.
return { candidates: allDefs, tier: 'global' };
};
const resolve = (name: string, fromFile: string): TieredCandidates | null => {
// Check cache (only when enabled AND fromFile matches cached file)
if (cache && cacheFile === fromFile) {
if (cache.has(name)) {
cacheHits++;
return cache.get(name)!;
}
cacheMisses++;
}
const result = resolveUncached(name, fromFile);
// Store in cache if active and file matches
if (cache && cacheFile === fromFile) {
cache.set(name, result);
}
return result;
};
// --- Cache lifecycle ---
const enableCache = (filePath: string): void => {
cacheFile = filePath;
if (!cache) cache = new Map();
else cache.clear();
};
const clearCache = (): void => {
cacheFile = null;
// Reuse the Map instance — just clear entries to reduce GC pressure at scale.
cache?.clear();
};
const getStats = () => ({
...symbols.getStats(),
cacheHits,
cacheMisses,
});
const clear = (): void => {
symbols.clear();
importMap.clear();
packageMap.clear();
namedImportMap.clear();
moduleAliasMap.clear();
clearCache();
cacheHits = 0;
cacheMisses = 0;
};
return {
resolve,
symbols,
importMap,
packageMap,
namedImportMap,
moduleAliasMap,
enableCache,
clearCache,
getStats,
clear,
};
};

View file

@ -0,0 +1,345 @@
import type { NodeLabel } from 'gitnexus-shared';
export const CLASS_TYPES = new Set(['Class', 'Struct', 'Interface', 'Enum', 'Record']);
export interface SymbolDefinition {
nodeId: string;
filePath: string;
type: NodeLabel;
/** Canonical dot-separated qualified type name for class-like symbols
* (e.g. `App.Models.User`). Falls back to the simple symbol name when no
* package/namespace/module scope exists or no explicit qualified metadata is provided. */
qualifiedName?: string;
parameterCount?: number;
/** Number of required (non-optional, non-default) parameters.
* Enables range-based arity filtering: argCount >= requiredParameterCount && argCount <= parameterCount. */
requiredParameterCount?: number;
/** Per-parameter type names for overload disambiguation (e.g. ['int', 'String']).
* Populated when parameter types are resolvable from AST (any typed language).
* Used for disambiguation in overloading languages (Java, Kotlin, C#, C++). */
parameterTypes?: string[];
/** Raw return type text extracted from AST (e.g. 'User', 'Promise<User>') */
returnType?: string;
/** Declared type for non-callable symbols — fields/properties (e.g. 'Address', 'List<User>') */
declaredType?: string;
/** Links Method/Constructor/Property to owning Class/Struct/Trait nodeId */
ownerId?: string;
}
export interface SymbolTable {
/**
* Register a new symbol definition
*/
add: (
filePath: string,
name: string,
nodeId: string,
type: NodeLabel,
metadata?: {
parameterCount?: number;
requiredParameterCount?: number;
parameterTypes?: string[];
returnType?: string;
declaredType?: string;
ownerId?: string;
qualifiedName?: string;
},
) => void;
/**
* High Confidence: Look for a symbol specifically inside a file
* Returns the Node ID if found
*/
lookupExact: (filePath: string, name: string) => string | undefined;
/**
* High Confidence: Look for a symbol in a specific file, returning full definition.
* Includes type information needed for heritage resolution (Class vs Interface).
* Returns first matching definition — use lookupExactAll for overloaded methods.
*/
lookupExactFull: (filePath: string, name: string) => SymbolDefinition | undefined;
/**
* High Confidence: Look for ALL symbols with this name in a specific file.
* Returns all definitions, including overloaded methods with the same name.
* Used by resolution-context to pass all same-file overloads to candidate filtering.
*/
lookupExactAll: (filePath: string, name: string) => SymbolDefinition[];
/**
* Low Confidence: Look for a symbol anywhere in the project
* Used when imports are missing or for framework magic
*/
lookupFuzzy: (name: string) => SymbolDefinition[];
/**
* Low Confidence: Look for callable symbols (Function/Method/Constructor) by name.
* Faster than `lookupFuzzy` + filter — backed by a lazy callable-only index.
* Used by ReturnTypeLookup to resolve callee → return type.
*/
lookupFuzzyCallable: (name: string) => SymbolDefinition[];
/**
* Look up a field/property by its owning class nodeId and field name.
* O(1) via dedicated eagerly-populated index keyed by `ownerNodeId\0fieldName`.
* Returns undefined when no matching property exists or the owner is ambiguous.
*/
lookupFieldByOwner: (ownerNodeId: string, fieldName: string) => SymbolDefinition | undefined;
/**
* Look up a method by its owning class nodeId and method name.
* O(1) via dedicated eagerly-populated index keyed by `ownerNodeId\0methodName`.
* For overloaded methods (same owner + name): returns the first match when all
* overloads share the same returnType, undefined when return types differ (ambiguous).
* Used by walkMixedChain for deterministic cross-class chain resolution.
*/
lookupMethodByOwner: (ownerNodeId: string, methodName: string) => SymbolDefinition | undefined;
/**
* Look up class-like definitions (Class, Struct, Interface, Enum, Record) by name.
* O(1) via dedicated eagerly-populated index keyed by symbol name.
* Returns all matching definitions across files (e.g. partial classes).
* Used by Phase 1 semantic-model tasks to replace filtered lookupFuzzy calls.
*/
lookupClassByName: (name: string) => SymbolDefinition[];
/**
* Look up class-like definitions by canonical qualified name.
* Qualified names are normalized to dot-separated scope segments across languages,
* e.g. `App.Models.User`, `com.example.User`, or `Admin.User`.
* Top-level class-like symbols with no explicit scope are indexed under their simple name.
*/
lookupClassByQualifiedName: (qualifiedName: string) => SymbolDefinition[];
/**
* Debugging: See how many symbols are tracked
*/
getStats: () => { fileCount: number; globalSymbolCount: number };
/**
* Cleanup memory
*/
clear: () => void;
}
export const createSymbolTable = (): SymbolTable => {
// 1. File-Specific Index — stores full SymbolDefinition(s) for O(1) lookup.
// Structure: FilePath -> (SymbolName -> SymbolDefinition[])
// Array allows overloaded methods (same name, different signatures) to coexist.
const fileIndex = new Map<string, Map<string, SymbolDefinition[]>>();
// 2. Global Reverse Index (The "Backup")
// Structure: SymbolName -> [List of Definitions]
const globalIndex = new Map<string, SymbolDefinition[]>();
// 3. Lazy Callable Index — populated on first lookupFuzzyCallable call.
// Structure: SymbolName -> [Callable Definitions]
// Only Function, Method, Constructor symbols are indexed.
let callableIndex: Map<string, SymbolDefinition[]> | null = null;
// 4. Eagerly-populated Field/Property Index — keyed by "ownerNodeId\0fieldName".
// Only Property symbols with ownerId and declaredType are indexed.
const fieldByOwner = new Map<string, SymbolDefinition>();
// 5. Eagerly-populated Method Index — keyed by "ownerNodeId\0methodName".
// Method symbols with ownerId are indexed. Supports overloads (array values).
const methodByOwner = new Map<string, SymbolDefinition[]>();
// 6. Eagerly-populated Class-type Index — keyed by symbol name.
// Only Class, Struct, Interface, Enum, Record symbols are indexed.
const classByName = new Map<string, SymbolDefinition[]>();
const classByQualifiedName = new Map<string, SymbolDefinition[]>();
const CALLABLE_TYPES = new Set(['Function', 'Method', 'Constructor']);
const add = (
filePath: string,
name: string,
nodeId: string,
type: NodeLabel,
metadata?: {
parameterCount?: number;
requiredParameterCount?: number;
parameterTypes?: string[];
returnType?: string;
declaredType?: string;
ownerId?: string;
qualifiedName?: string;
},
) => {
const qualifiedName = CLASS_TYPES.has(type)
? (metadata?.qualifiedName ?? name)
: metadata?.qualifiedName;
const def: SymbolDefinition = {
nodeId,
filePath,
type,
...(qualifiedName !== undefined ? { qualifiedName } : {}),
...(metadata?.parameterCount !== undefined
? { parameterCount: metadata.parameterCount }
: {}),
...(metadata?.requiredParameterCount !== undefined
? { requiredParameterCount: metadata.requiredParameterCount }
: {}),
...(metadata?.parameterTypes !== undefined
? { parameterTypes: metadata.parameterTypes }
: {}),
...(metadata?.returnType !== undefined ? { returnType: metadata.returnType } : {}),
...(metadata?.declaredType !== undefined ? { declaredType: metadata.declaredType } : {}),
...(metadata?.ownerId !== undefined ? { ownerId: metadata.ownerId } : {}),
};
// A. Add to File Index (shared reference — zero additional memory)
if (!fileIndex.has(filePath)) {
fileIndex.set(filePath, new Map());
}
const fileMap = fileIndex.get(filePath)!;
if (!fileMap.has(name)) {
fileMap.set(name, [def]);
} else {
fileMap.get(name)!.push(def);
}
// B. Properties go to fieldByOwner index only — skip globalIndex to prevent
// namespace pollution for common names like 'id', 'name', 'type'.
// Index ALL properties (even without declaredType) so write-access tracking
// can resolve field ownership for dynamically-typed languages (Ruby, JS).
if (type === 'Property' && metadata?.ownerId) {
fieldByOwner.set(`${metadata.ownerId}\0${name}`, def);
// Still add to fileIndex above (for lookupExact), but skip globalIndex
return;
}
// C. Add to Global Index (same object reference)
if (!globalIndex.has(name)) {
globalIndex.set(name, []);
}
globalIndex.get(name)!.push(def);
// C2. Methods with ownerId go to methodByOwner index (in addition to globalIndex).
if (type === 'Method' && metadata?.ownerId) {
const key = `${metadata.ownerId}\0${name}`;
const existing = methodByOwner.get(key);
if (existing) {
existing.push(def);
} else {
methodByOwner.set(key, [def]);
}
}
// C3. Class-like types go to classByName index (in addition to globalIndex).
if (CLASS_TYPES.has(type)) {
const existing = classByName.get(name);
if (existing) {
existing.push(def);
} else {
classByName.set(name, [def]);
}
const qualifiedKey = qualifiedName ?? name;
const qualifiedMatches = classByQualifiedName.get(qualifiedKey);
if (qualifiedMatches) {
qualifiedMatches.push(def);
} else {
classByQualifiedName.set(qualifiedKey, [def]);
}
}
// D. Invalidate the lazy callable index only when adding callable types
if (CALLABLE_TYPES.has(type)) {
callableIndex = null;
}
};
const lookupExact = (filePath: string, name: string): string | undefined => {
const defs = fileIndex.get(filePath)?.get(name);
return defs?.[0]?.nodeId;
};
const lookupExactFull = (filePath: string, name: string): SymbolDefinition | undefined => {
const defs = fileIndex.get(filePath)?.get(name);
return defs?.[0];
};
const lookupExactAll = (filePath: string, name: string): SymbolDefinition[] => {
return fileIndex.get(filePath)?.get(name) ?? [];
};
const lookupFuzzy = (name: string): SymbolDefinition[] => {
return globalIndex.get(name) || [];
};
const lookupFuzzyCallable = (name: string): SymbolDefinition[] => {
if (!callableIndex) {
// Build the callable index lazily on first use
callableIndex = new Map();
for (const [symName, defs] of globalIndex) {
const callables = defs.filter((d) => CALLABLE_TYPES.has(d.type));
if (callables.length > 0) callableIndex.set(symName, callables);
}
}
return callableIndex.get(name) ?? [];
};
const lookupFieldByOwner = (
ownerNodeId: string,
fieldName: string,
): SymbolDefinition | undefined => {
return fieldByOwner.get(`${ownerNodeId}\0${fieldName}`);
};
const lookupMethodByOwner = (
ownerNodeId: string,
methodName: string,
): SymbolDefinition | undefined => {
const defs = methodByOwner.get(`${ownerNodeId}\0${methodName}`);
if (!defs || defs.length === 0) return undefined;
if (defs.length === 1) return defs[0];
// Multiple overloads: return first if all share the same defined returnType (safe for chain resolution).
// Return undefined if return types differ or are absent (truly ambiguous — can't determine which overload).
const firstReturnType = defs[0].returnType;
if (firstReturnType === undefined) return undefined;
for (let i = 1; i < defs.length; i++) {
if (defs[i].returnType !== firstReturnType) return undefined;
}
return defs[0];
};
const lookupClassByName = (name: string): SymbolDefinition[] => {
return classByName.get(name) ?? [];
};
const lookupClassByQualifiedName = (qualifiedName: string): SymbolDefinition[] => {
return classByQualifiedName.get(qualifiedName) ?? [];
};
const getStats = () => ({
fileCount: fileIndex.size,
globalSymbolCount: globalIndex.size,
});
const clear = () => {
fileIndex.clear();
globalIndex.clear();
callableIndex = null;
fieldByOwner.clear();
methodByOwner.clear();
classByName.clear();
classByQualifiedName.clear();
};
return {
add,
lookupExact,
lookupExactFull,
lookupExactAll,
lookupFuzzy,
lookupFuzzyCallable,
lookupFieldByOwner,
lookupMethodByOwner,
lookupClassByName,
lookupClassByQualifiedName,
getStats,
clear,
};
};

File diff suppressed because it is too large Load diff