GitNexus Documentation

GitNexus: The Zero-Server Code Intelligence Engine

47,840 stars TypeScript TypeScript 1 file ยท ~49,287 tokens
RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy GitNexus on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name gitnexus -p 4747:4747 --restart always ${SERVER_IMAGE:-ghcr.io/abhigyanpatwari/gitnexus:latest}
Configured Services & Ports
โš™๏ธ gitnexus-server
image: ${SERVER_IMAGE:-ghcr.io/abhigyanpatwari/gitnexus:latest}
๐Ÿ”Œ Ports: ${SERVER_HOST_PORT:-4747}:4747
โš™๏ธ gitnexus-web
image: ${WEB_IMAGE:-ghcr.io/abhigyanpatwari/gitnexus-web:latest}
๐Ÿ”Œ Ports: ${WEB_HOST_PORT:-4173}:4173
docker-compose.yml
yaml (docker-compose.yml)
services:
  gitnexus-server:
    image: ${SERVER_IMAGE:-ghcr.io/abhigyanpatwari/gitnexus:latest}
    container_name: ${SERVER_CONTAINER_NAME:-gitnexus-server}
    # Map the server to the same host port the web UI expects by default
    # (http://localhost:4747). The browser runs on the host, so the UI's
    # built-in default works without any reconfiguration.
    ports:
      - '${SERVER_HOST_PORT:-4747}:4747'
    volumes:
      # Persist the global registry, indexes, and cloned repos across runs.
      - gitnexus-data:/data/gitnexus
      # Optional: mount a host workspace so `gitnexus index ` can see
      # repos you already have on disk. The default points at an empty
      # `./workspace/` sibling that compose will create on first start โ€”
      # it intentionally does NOT bind-mount the repo root, which would
      # expose `.git`, `.env`, and CI secrets to the container.
      # Override with `WORKSPACE_DIR=/abs/path/to/your/repos`.
      - ${WORKSPACE_DIR:-./workspace}:/workspace:ro
    restart: unless-stopped
    healthcheck:
      test: ['CMD', 'curl', '-f', 'http://localhost:4747/api/health']
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 15s

  gitnexus-web:
    image: ${WEB_IMAGE:-ghcr.io/abhigyanpatwari/gitnexus-web:latest}
    container_name: ${WEB_CONTAINER_NAME:-gitnexus-web}
    ports:
      - '${WEB_HOST_PORT:-4173}:4173'
    # Override the backend URL served to the browser. The default
    # (http://localhost:4747) works when both containers run locally.
    # Set GITNEXUS_BACKEND_URL in your .env or shell for remote/custom setups:
    #   GITNEXUS_BACKEND_URL=http://:4747
    environment:
      - GITNEXUS_BACKEND_URL=${GITNEXUS_BACKEND_URL:-}
    depends_on:
      gitnexus-server:
        condition: service_healthy
    restart: unless-stopped
    healthcheck:
      test: ['CMD', 'curl', '-f', 'http://localhost:4173/']
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s

volumes:
  gitnexus-data:

Technical Documentation: abhigyanpatwari/GitNexus

โ„น๏ธ Provenance: Multi-Tier Ingestion ยท CodeWiki Reference

Overview (README.md)

GitNexus

https://github.com/user-attachments/assets/172685ba-8e54-4ea7-9ad1-e31a3398da72

Like DeepWiki, but deeper. DeepWiki helps you understand code. GitNexus lets you analyze it โ€” a knowledge graph tracks every relationship, not just descriptions.

TL;DR: The CLI + MCP makes your AI agent reliable โ€” it gives Cursor, Claude Code, Antigravity, Codex, and friends a deep architectural view of your codebase so they stop missing dependencies, breaking call chains, and shipping blind edits. Even smaller models get full architectural clarity. The Web UI is a quick way to chat with any repo in the browser.

Quick Start

bash
# 1. Index your repo (run from repo root)
npx gitnexus analyze

# 2. Connect your editors (one-time, auto-detects Claude Code, Cursor, Codex, โ€ฆ)
npx gitnexus setup

That's it. analyze indexes the codebase, installs agent skills, registers Claude Code hooks, and creates AGENTS.md / CLAUDE.md context files โ€” all in one command. setup writes the MCP config so your AI agent can use the graph.

Install problems? npm 11 crash ยท slow cold install ยท no C++ toolchain

On npm 11.x? npx can crash during install with Cannot destructure property 'package' of 'node.target' (an npm/arborist bug, before GitNexus runs). Use pnpm instead โ€” it builds the native deps explicitly:

bash
pnpm --allow-build=@ladybugdb/core --allow-build=gitnexus --allow-build=tree-sitter dlx gitnexus@latest analyze

Or install globally (npm install gitnexus@latest) and run gitnexus analyze. See #1939.

Fastest MCP startup: install globally (npm i gitnexus) before running gitnexus setup โ€” this writes an absolute-path MCP config that bypasses npx entirely. On a cold cache, an npx-based MCP install can exceed Claude Code's MCP_TIMEOUT default (~30s).

No C++ toolchain? Set GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1 before npm install gitnexus to skip the vendored grammar materialize/build for tree-sitter-dart, tree-sitter-proto, tree-sitter-swift, and tree-sitter-kotlin โ€” those four languages won't be parsed, but install completes in seconds without python3/make/g++. Strict =1 only โ€” any other value falls through to the rebuild.

Local embeddings are opt-in. Default npm install does not fetch @huggingface/transformers or onnxruntime-node. Run gitnexus embeddings install (or gitnexus analyze --embeddings, which auto-heals) to fetch the stack through your npm registry config into ~/.gitnexus/embedding-runtime. CUDA GPU binaries still use NuGet via --cuda (#2370). The prefix needs Node with module.registerHooks (โ‰ฅ 22.15 on 22.x, โ‰ฅ 23.5 on 23.x). A leftover 1.6.12 package-first tree is residual until a clean reinstall; --force only refreshes prefix overrides.

About tree-sitter-kotlin: like Dart/Proto/Swift, Kotlin is a vendored grammar (under gitnexus/vendor/tree-sitter-kotlin). Upstream ships source only (no prebuilt binaries), so GitNexus cross-builds the platform prebuilds itself (via the build-tree-sitter-prebuilds GitHub Actions workflow) and vendors them โ€” the same uniform pipeline used for Dart, Proto, and Swift. node-gyp-build selects the right .node at require time, so no C/C++ toolchain is needed. If no prebuild matches your platform-arch, only Kotlin (.kt/.kts) parsing is unavailable; the rest of gitnexus is unaffected.

Deploy to Render

Deploy GitNexus in one click:

The Blueprint creates two services. gitnexus-server runs gitnexus serve as a private service: no public URL, reachable only over Render's private network, with a persistent disk for indexes and cloned repos. gitnexus-web is the public one. It serves the UI and reverse-proxies /api/* to the server, so the browser talks to a single origin.

At the Blueprint's defaults this runs about $35/month: $25 for the server's standard instance, $7 for the web service's starter instance, and $2.50 for the 10 GB disk. See Render's pricing for other plans.

The deploy generates an access token, and the UI asks for it on first use:

  1. Open the gitnexus-web service in your Render dashboard.
  2. Copy GITNEXUS_SERVE_AUTH_TOKEN from its Environment tab.
  3. Load the site and paste the token into the prompt (or the settings panel).

Every /api/* request carries that token as a header, and the proxy answers 401 without it. The browser keeps it in sessionStorage, so a new tab asks again. To rotate it, edit the environment variable and redeploy.

The proxy strips Origin before forwarding, so the server's CSRF guard does nothing for proxied traffic; it passes Origin-less requests through by design. The token is the only control on this deploy, not a second layer behind the guard. Anyone holding it can read every indexed repo. See SECURITY.md.

Indexing is memory-bound. If gitnexus-server runs out of memory on a large repo, raise its plan, which sets available RAM: standard is 2 GB, pro is 4 GB. Raise sizeGB only if the disk fills with clones and indexes.

Deploy to RepoCloud

Two Ways to Use GitNexus

CLI + MCP (recommended) Web UI
What Index repos locally, connect AI agents via MCP Visual graph explorer + AI chat in browser
For Daily development with Cursor, Claude Code, Antigravity, Codex, Windsurf, OpenCode Quick exploration, demos, one-off analysis
Scale Full repos, any size Limited by browser memory (~5k files), or unlimited via backend mode
Install npm install gitnexus No install โ€” gitnexus.vercel.app
Storage LadybugDB native (fast, persistent) LadybugDB WASM (in-memory, per session)
Parsing Tree-sitter native bindings Tree-sitter WASM
Privacy Everything local, no network Everything in-browser, no server

Bridge mode: gitnexus serve connects the two โ€” the web UI auto-detects the local server and can browse all your CLI-indexed repos without re-uploading or re-indexing.

Why a Knowledge Graph?

Tools like Cursor, Claude Code, Codex, Cline, Roo Code, and Windsurf are powerful โ€” but they don't truly know your codebase structure. So this happens:

  1. AI edits UserService.validate()
  2. Doesn't know 47 functions depend on its return type
  3. Breaking changes ship

Traditional Graph RAG gives the LLM raw graph edges and hopes it explores enough. GitNexus precomputes structure at index time โ€” clustering, tracing, scoring โ€” so tools return complete context in one call:

mermaid
flowchart TB
    subgraph Traditional["Traditional Graph RAG"]
        direction TB
        U1["User: What depends on UserService?"]
        U1 --> LLM1["LLM receives raw graph"]
        LLM1 --> Q1["Query 1: Find callers"]
        Q1 --> Q2["Query 2: What files?"]
        Q2 --> Q3["Query 3: Filter tests?"]
        Q3 --> Q4["Query 4: High-risk?"]
        Q4 --> OUT1["Answer after 4+ queries"]
    end

    subgraph GN["GitNexus Smart Tools"]
        direction TB
        U2["User: What depends on UserService?"]
        U2 --> TOOL["impact UserService upstream"]
        TOOL --> PRECOMP["Pre-structured response:
        8 callers, 3 clusters, all 90%+ confidence"]
        PRECOMP --> OUT2["Complete answer, 1 query"]
    end

Core innovation: Precomputed Relational Intelligence

  • Reliability โ€” the LLM can't miss context; it's already in the tool response
  • Token efficiency โ€” no 10-query chains to understand one function
  • Model democratization โ€” smaller LLMs work because the tools do the heavy lifting

What Your AI Agent Gets

19 MCP tools (17 per-repo + 2 group)

Tool What It Does
list_repos Discover all indexed repositories (paginated โ€” limit/offset)
query Process-grouped hybrid search (BM25 + semantic + RRF)
context 360-degree symbol view โ€” categorized refs, process participation
impact Blast radius analysis with depth grouping and confidence
trace Shortest directed path between two symbols (call + class-member edges)
detect_changes Git-diff impact โ€” maps changed lines to affected processes
check Read-only structural checks against the indexed graph
rename Multi-file coordinated rename with graph + text search
cypher Raw Cypher graph queries
route_map API route map โ€” which components fetch which endpoints, and handlers
tool_map MCP/RPC tool definitions โ€” where they're defined and handled
shape_check Validate API response shapes against consumers' property accesses
api_impact Pre-change impact report for an API route handler
explain Explain persisted taint findings (sourceโ†’sink flows, --pdg indexes)
pdg_query Query control/data dependence at statement level (--pdg indexes)
read_file Read a checkout file (optional 0-indexed slice; maxLines cap)
grep Regex search of the working tree for indexed files (1-based hits)
group_list List configured repository groups
group_sync Rebuild a group's Contract Registry and cross-repo links

Per-repo read-only tools take an optional repo parameter. Omit it when only one repo is indexed, an MCP default is configured, or the GitNexus process cwd is inside a registered path without crossing into an unindexed nested Git checkout; otherwise pass it explicitly. Mutating tools require repo when multiple repos are indexed and no MCP default exists. Per-repo tools also take an optional branch for indexes pinned with gitnexus analyze --branch, except read_file and grep, which read the checkout and do not accept branch. Omitting branch queries the workspace index, which follows your checked-out working tree โ€” switching branches and re-running gitnexus analyze updates it incrementally. explain and pdg_query need an index built with gitnexus analyze --pdg.

Resources for instant context

Resource Purpose
gitnexus://repos List all indexed repositories (read this first)
gitnexus://setup Setup and usage guidance for agents
gitnexus://repo/{name}/context Codebase stats, staleness check, and available tools
gitnexus://repo/{name}/clusters All functional clusters with cohesion scores
gitnexus://repo/{name}/cluster/{name} Cluster members and details
gitnexus://repo/{name}/processes All execution flows
gitnexus://repo/{name}/process/{name} Full process trace with steps
gitnexus://repo/{name}/schema Graph schema for Cypher queries
gitnexus://group/{name}/contracts A group's extracted contracts and cross-links
gitnexus://group/{name}/status Staleness of repos in a group

2 MCP prompts for guided workflows

Prompt What It Does
detect_impact Pre-commit change analysis โ€” scope, affected processes, risk level
generate_map Architecture documentation from the knowledge graph with mermaid diagrams

Agent skills installed to `.claude/skills/` and `.agents/skills/` (if `.agents/` exists) automatically

  • Exploring โ€” navigate unfamiliar code using the knowledge graph
  • Debugging โ€” trace bugs through call chains
  • Impact Analysis โ€” analyze blast radius before changes
  • Refactoring โ€” plan safe refactors using dependency mapping
  • Guide โ€” GitNexus tool/resource/schema reference for the agent
  • CLI โ€” run analyze/status/clean/wiki commands on request
  • PDG Query โ€” statement-level control/data dependence queries (--pdg index)
  • Taint Analysis โ€” sourceโ†’sink data-flow findings (--pdg index)
  • Plan (/gitnexus-plan) โ€” implementation-ready engineering plans backed by the graph and PDG slices
  • Work (/gitnexus-work) โ€” executes a plan as impact-checked, detect_changes-gated atomic commits
  • Review (/gitnexus-review) โ€” graph-backed review of a PR, branch, range, or local diff, with taint pass and per-domain expert lenses
  • LFG (/gitnexus-lfg) โ€” the full pipeline: plan โ†’ user gate โ†’ work โ†’ review

Repo-specific skills โ€” run gitnexus analyze --skills and GitNexus detects the functional areas of your codebase (via Leiden community detection) and generates each one as a direct project skill under .claude/skills/gitnexus-area-<name>/. Each skill describes a module's key files, entry points, execution flows, and cross-area connections, and is regenerated on each --skills run to stay current.

When a repo contains an .agents/ directory, the standard and generated skills are also mirrored to .agents/skills/ (e.g. .agents/skills/gitnexus-cli/, .agents/skills/gitnexus-area-<name>/) so agents that read repo-local .agents/skills/ (like Codex) stay in sync.

Editor Setup

gitnexus setup auto-detects your editors and writes the correct global MCP config. Run it once. To configure only selected integrations, pass --coding-agent/-c with a comma-separated list, e.g. gitnexus setup -c cursor,codex.

Editor MCP Skills Hooks (auto-augment) Support
Claude Code Yes Yes Yes (PreToolUse + PostToolUse) Full
Cursor Yes Yes Yes (postToolUse, manual install) Full
Antigravity (Google) Yes Yes Yes (AfterTool, Gemini CLI hooks schema)ยน Full
Codex Yes Yes Yes (PreToolUse + PostToolUse, Codex hooks) Full
Factory (Droid) Yes Yes Yes (PostToolUse, plugin) Full
OpenCode Yes Yes โ€” MCP + Skills
CodeBuddy (Tencent) Yes Yes โ€” MCP + Skills
Qoder (Alibaba) Yes Yes โ€” MCP + Skills
Windsurf Yes โ€” โ€” MCP

Full means MCP tools + agent skills + hooks that enrich searches with graph context. Claude Code and Codex go deepest: their PreToolUse hooks enrich the search before it runs, and their PostToolUse hooks also detect a stale index after commits and prompt the agent to reindex. Cursor, Antigravity, and Factory augment from a post-tool hook only, so they enrich the result rather than the query and do not carry the stale-index hint.

ยน Antigravity hooks follow the Gemini CLI hooks reference (Antigravity 2.0 is the documented successor to Gemini CLI). Augmentation runs in AfterTool because BeforeTool has no context-injection channel in the Gemini contract โ€” the agent sees graph context appended to the tool result via hookSpecificOutput.additionalContext. Stale-index hints land in the same channel after a successful git commit/merge/rebase/cherry-pick/pull. The schema may evolve if Antigravity-specific hook docs diverge from Gemini CLI's; the implementation will track those changes.

Manual MCP configuration (if you prefer not to run gitnexus setup)

Claude Code (full support โ€” MCP + skills + hooks):

bash
# macOS / Linux
claude mcp add gitnexus -- npx -y gitnexus@latest mcp

# Windows
claude mcp add gitnexus -- cmd /c npx -y gitnexus@latest mcp

Codex (full support โ€” MCP + skills + hooks):

bash
codex mcp add gitnexus -- npx -y gitnexus@latest mcp

Or via ~/.codex/config.toml (system scope) / .codex/config.toml (project scope):

toml
[mcp_servers.gitnexus]
command = "npx"
args = ["-y", "gitnexus@latest", "mcp"]

Codex hooks (PreToolUse graph enrichment + PostToolUse stale-index detection in ~/.codex/hooks.json, same schema as Claude Code) need the bundled adapter script, so they are installed by gitnexus setup -c codex rather than manually.

Alternatively, install everything as a Codex plugin (MCP + skills + hooks in one step):

bash
codex plugin marketplace add abhigyanpatwari/GitNexus
# then inside Codex: /plugins โ†’ install "GitNexus"

Codex notes: SessionStart is intentionally not registered โ€” Codex reads AGENTS.md natively, which already carries the GitNexus context block. Newly installed hooks need a one-time approval in Codex via /hooks before they run. Pick one install route (gitnexus setup -c codex or the plugin): plugin hooks load alongside ~/.codex/hooks.json, so installing both can fire duplicate hooks per tool call.

Factory (Droid) โ€” MCP + skills via gitnexus setup -c droid, or add the server manually to ~/.factory/mcp.json (user scope, applies to all projects):

json
{
  "mcpServers": {
    "gitnexus": {
      "command": "npx",
      "args": ["-y", "gitnexus@latest", "mcp"]
    }
  }
}

gitnexus setup -c droid also installs skills to ~/.factory/skills/. For the PostToolUse search-augment hook, install the bundled gitnexus-factory-plugin/ โ€” from a marketplace that includes this repo, run droid plugin install gitnexus@<marketplace>, or point Droid at it via extraKnownMarketplaces in .factory/settings.json. Factory reads AGENTS.md natively, which already carries the GitNexus context block.

Cursor (~/.cursor/mcp.json โ€” global, works for all projects):

json
{
  "mcpServers": {
    "gitnexus": {
      "command": "npx",
      "args": ["-y", "gitnexus@latest", "mcp"]
    }
  }
}

Antigravity (Google) โ€” ~/.gemini/antigravity/mcp_config.json:

json
{
  "mcpServers": {
    "gitnexus": {
      "command": "npx",
      "args": ["-y", "gitnexus@latest", "mcp"]
    }
  }
}

gitnexus setup also merges an AfterTool entry into ~/.gemini/settings.json (under the canonical Gemini CLI hooks schema) and installs skills to ~/.gemini/antigravity/skills/. Existing user hooks are preserved. The hook adapter's path is rewritten at install time, so run gitnexus setup rather than hand-editing.

OpenCode (~/.config/opencode/config.json):

json
{
  "mcp": {
    "gitnexus": {
      "type": "local",
      "command": ["gitnexus", "mcp"]
    }
  }
}

CodeBuddy (Tencent) โ€” priority chain, edit the first non-empty file that exists: ~/.codebuddy/.mcp.json (recommended) โ†’ ~/.codebuddy/mcp.json (deprecated) โ†’ ~/.codebuddy.json (legacy). CodeBuddy reads only the first existing file, so adding servers to a higher-priority file than the one currently in use would hide the servers below it. Create ~/.codebuddy/.mcp.json only if none exist:

json
{
  "mcpServers": {
    "gitnexus": {
      "command": "npx",
      "args": ["-y", "gitnexus@latest", "mcp"]
    }
  }
}

Qoder (Alibaba) โ€” ~/.qoder.json:

json
{
  "mcpServers": {
    "gitnexus": {
      "command": "npx",
      "args": ["-y", "gitnexus@latest", "mcp"]
    }
  }
}

MCP read-only mode

Set GITNEXUS_MCP_READ_ONLY=1 before starting the MCP server to expose only the proven single-repository read surface. Raw cypher, rename and group tools, group routing, and group resources are omitted from discovery and rejected before backend dispatch. Tool descriptions and generated setup/context resources are scrubbed so they do not recommend unavailable routes.

The default is unchanged when the variable is unset or 0. Any other value fails server startup rather than silently weakening the policy.

MCP repository policy

Set GITNEXUS_MCP_ALLOWED_REPOS to a comma-separated list of canonical registry names or absolute indexed paths. Entries are trimmed, resolved against the registry, and deduplicated at startup. When exactly one repository is allowed it becomes the implicit default; when several are allowed, callers must select one unless GITNEXUS_MCP_DEFAULT_REPO is also set.

The default repository must resolve to an allowed repository. Invalid, ambiguous, blank, or mismatched configuration fails startup before stdio or HTTP begins serving. The allowlist applies to tools, aliases, discovery, resources, templates, implicit resolution, and embedded HTTP; hidden repository details are not included in selection errors. Setting only GITNEXUS_MCP_DEFAULT_REPO chooses a default without restricting explicit repository selections. An allowed repository whose name is duplicated in the registry must be configured by path, and its context resource is only served for the unique name form.

MCP response budgets

The query, context, and impact tools accept an optional positive-integer maxTokens argument. It bounds the complete formatted MCP response, including hints and error text, using a deterministic four-UTF-8-bytes-per-token estimate. When truncation is required, the response ends with โ€ฆ and remains valid UTF-8.

Set GITNEXUS_MCP_DEFAULT_MAX_TOKENS to apply the same guardrail when callers do not send maxTokens. An explicit tool argument takes precedence. Leaving both unset preserves the existing response byte-for-byte; this is a transport guardrail, not semantic pagination or an exact model-specific tokenizer limit.

CLI Reference

Everyday commands:

bash
gitnexus setup                   # Configure MCP for detected editors (one-time; -c to select)
gitnexus analyze [path]          # Index a repository (or update a stale index)
gitnexus analyze [path] --watch  # Watch local files and serialize incremental refreshes
gitnexus mcp                     # Start MCP server (stdio) โ€” serves all indexed repos
gitnexus serve                   # Start local HTTP server (multi-repo) for web UI connection
gitnexus eval-server             # Start lightweight evaluation HTTP tools (loopback by default)
gitnexus list                    # List all indexed repositories
gitnexus status                  # Show index status for current repo
gitnexus clean                   # Delete index for current repo
gitnexus wiki [path]             # Generate repository wiki from knowledge graph
gitnexus uninstall               # Preview removal of GitNexus MCP/skills/hooks (--force to apply)

You can also query the graph directly from the terminal โ€” gitnexus query, context, impact, trace, cypher, detect-changes, and check mirror the MCP tools of the same names, and gitnexus doctor prints runtime platform capabilities.

gitnexus analyze --watch requires a Git repository. It runs one initial
analysis, then debounces scanner-admitted working-tree changes for 300 ms by
default and applies serialized incremental refreshes. Events arriving during a
refresh remain queued, and retryable failures retain the same batch with bounded
backoff. Invalid .gitnexusrc or ignore-file reloads pause ordinary refreshes
until the control file is fixed. Stop the watcher with Ctrl+C.

Watch mode accepts --debounce, --workers, --worker-timeout,
--max-file-size, --max-processes, --max-process-branching,
--max-process-trace-depth, --max-entry-point-candidates, --branch, --pdg, --skip-fts, --name, --allow-duplicate-name, and
--verbose. Explicit one-shot options such as --force, --repair-fts,
embedding flags, --skills, --self-commit, --index-only, and --skip-git
are rejected. Unsupported defaults from .gitnexusrc are ignored with a
warning rather than making an otherwise valid repository unwatchable.

POSIX requests clone-first copy-and-swap publication when the live index has no
orphan sidecars. Windows and sidecar fallback runs update in place: failures
known to occur before writes are retried, while a failure that may have mutated
the live index stops the watcher. Watch mode does not pull remotes. Running MCP
and serve processes reopen a newly published index automatically; MCP observes
the replacement on its next tool call, typically within five seconds, so no
restart is required.

Authenticated eval-server binding

gitnexus eval-server binds to 127.0.0.1 by default. Loopback bindings do not require authentication. Any non-loopback bind, including 0.0.0.0, a LAN address, or a hostname that resolves to a LAN IPv4 address, requires GITNEXUS_AUTH_TOKEN. Every endpoint then requires an exact Authorization: Bearer <token> header.

bash
GITNEXUS_AUTH_TOKEN='replace-me' gitnexus eval-server --host 0.0.0.0

The token may be set in the shell, .env.local, or .env in the working directory. Precedence is shell > .env.local > .env. Only GITNEXUS_AUTH_TOKEN is read from those files; their other values are not added to the process environment. Keep token files uncommitted.

All analyze flags

text
/* Detailed source-code truncated for AI context efficiency. */

--skip-fts (or GITNEXUS_SKIP_FTS=1) disables FTS extension loading and keyword-index construction for this analysis. Graph queries, communities, processes, and existing embeddings remain available. Status and search report "FTS disabled for this index". Remove both the flag and environment setting and run analyze again to restore keyword search, even at the same commit. Only the exact environment value 1 enables the opt-out; the flag takes precedence. It cannot be combined with --repair-fts. Disabling an existing FTS index may require one graph-store rebuild to avoid unsafe writes through native indexes.

--spring-actuator is explicitly opt-in and accepts either a JSON bundle keyed by mappings, beans, conditions, configprops, and/or env, or a directory containing endpoint-named JSON files. It confirms matching static nodes and adds conservative runtime-only routes, beans, and property keys. The configured input is excluded from source scanning; only normalized repository-relative exclusions are retained for future scans, never absolute paths. Env/configprops values, origins, condition messages, and source names are never persisted or printed. Because snapshots are external runtime state, an enabled run always rebuilds; the first later run without the option rebuilds once to remove runtime evidence. The same path can be set as springActuator in .gitnexusrc.

--asyncapi-spec is explicitly opt-in and accepts a directory of AsyncAPI documents or a single document; the path is resolved against the repository root, so a committed docs/asyncapi and an absolute cache written by something else both work. Each operations[] entry of an AsyncAPI 3.x document can contribute a Destination node keyed by broker and address, with action: send emitting PUBLISHES_TO and action: receive emitting CONSUMES_FROM, so a document and source code that name one address on one broker land on the same node. Edges start at the document, not at a callable โ€” a document states that the service talks to an address, not which method does โ€” and no address a document names is ever attached to an unresolved source site.

An operation must name a protocol, either through its own bindings or through the servers[].protocol of the servers its channel resolves to (a channel that lists no servers resolves to all of them); operations that name none are refused, as are operations whose two readings name different brokers, and channels that inherit a multi-protocol server set without choosing. HTTP and WebSocket documents are refused for destination minting: there the host rather than the address names the place, and an HTTP endpoint is already modelled as a Route. A parameterized address โ€” a channel declaring parameters, or an address containing { โ€” is refused rather than keyed: two services publishing {env}.orders share a pattern, not a queue. AsyncAPI 2.x is refused under its own counted reason and never mapped, because its publish/subscribe are inverted relative to 3.x send/receive and a naive mapping would reverse the async graph while leaving it connected. Every refusal is counted, and a configured path that yields nothing is reported rather than passed over in silence.

Like Actuator snapshots, documents are external to git freshness โ€” replacing one moves no commit and dirties no file โ€” so an enabled run always rebuilds, and the first later run without the option rebuilds once to remove document-derived evidence. There is no glob-based auto-discovery, and the option is unsupported with --watch.

If analyze reports a worker parse timeout on a large or unusual repository, it keeps running and falls back safely. To give slow worker jobs more time, use --worker-timeout 60 or set GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS=60000. For very large files, GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES controls the worker job byte budget.

Embeddings node limit โ€” gitnexus analyze --embeddings generates semantic search vectors with a default 50,000-node safety cap to protect memory on large repositories:

bash
gitnexus analyze --embeddings          # default 50,000 node safety cap
gitnexus analyze --embeddings 0        # disable the cap entirely
gitnexus analyze --embeddings 100000   # custom cap

If embeddings are skipped on a large repository, the indexed graph likely exceeds the default cap โ€” re-run with --embeddings 0 or a higher limit.

Keep remote repositories indexed with gitnexus auto-sync

gitnexus auto-sync clones or pulls configured repositories, analyzes new commits, and optionally syncs their group. It runs once immediately, then repeats on the configured interval. It runs in the foreground; use your process manager if it must survive a shell session. gitnexus watch is reserved and prints this split; it does not start auto-sync or local file watching.

bash
# 1. Create the config once. It never overwrites an existing file.
gitnexus auto-sync init

# 2. Edit $GITNEXUS_HOME/watch_config.yml, then start it.
gitnexus auto-sync start              # `gitnexus auto-sync` is equivalent
gitnexus auto-sync status
gitnexus auto-sync restart            # Required after config changes
gitnexus auto-sync stop
gitnexus auto-sync reset             # Clear failure state; leaves clones and indexes intact

GITNEXUS_HOME defaults to ~/.gitnexus. A minimal configuration:

yaml
sync_interval_minutes: 10
analyze_timeout: 5m
# Extra hosts beyond github.com, gitlab.com, and gitee.com. Exact names only.
# allowed_hosts: [gitlab.mycompany.com]
projects:
  - local_path: /absolute/path/to/clones
    branches: [main, master]
    # pdg: omit = preserve live index mode; true = keep PDG current;
    # false = init default (warns, then strips PDG on the next successful rebuild).
    # Do not paste pdg: false onto an existing watch file unless you intend to drop PDG.
    pdg: false
    overwrite_local_changes: false
    remote_urls:
      - [email protected]:owner/repo.git
  • sync_interval_minutes must be at least 5; local_path must be an absolute path. Clones are stored below it as host/namespace/repo.
  • Remote URLs may use SSH SCP or HTTPS. Hosts are github.com, gitlab.com, and gitee.com unless listed in top-level allowed_hosts (exact DNS names, no wildcards). The CLI image includes OpenSSH; mount keys yourself. Invalid watch_config.yml skips auto-sync immediately. Auto-sync honors .gitnexusrc embeddings (HTTP embeddings env still required in the image).
  • branches are tried in order. The legacy branch field is supported, but do not set both.
  • Set per-project pdg: true to keep the full control-flow, control/data-dependence, and taint layers current. Untouched configs that omit pdg preserve an existing index's mode and cannot silently strip PDG data. Do not paste pdg: false from this example onto an existing watch file unless you intend to drop PDG; an explicit false opt-out logs a warning before removing existing PDG data. Auto-sync requests atomic incremental publication where supported, so readers keep using the previous graph until a successful update is ready and a failed staged analysis leaves it intact; unsupported paths retain the analyzer's existing in-place behavior.
  • Analysis runs in an isolated worker; analyze_timeout defaults to half of sync_interval_minutes, but may be longer (for example, a 30m analysis timeout with 5 minute polling) up to Node's timer limit. If a polling tick arrives while analysis is active, it is coalesced into one immediate follow-up run using the newest commit. If the parent times out and leaves that worker running, the follow-up is deferred to the next interval so a leftover lock holder is not counted as a hard analyze failure. Timeout and auto-sync stop request safe cancellation; a worker in native work exits after reaching a JS-visible safe point. Until then, auto-sync reports cancelling or stopping and retains ownership so another auto-sync cannot take over, for up to 5 seconds โ€” after that the parent stops waiting and leaves the worker to exit on its own rather than killing it mid-write. This behavior is the same on macOS and Windows. overwrite_local_changes defaults to false, so a dirty local clone is skipped rather than overwritten; setting it to true also deletes untracked files in the clone, while keeping ignored paths.
  • Add group_name only after creating that group with gitnexus group create <name>. Partial clone output is isolated and removed after 14 days.

See the full auto-sync configuration and runtime reference for concurrency, timeouts, failure thresholds, and runtime files.

Repository groups (multi-repo / monorepo service tracking)

bash
gitnexus group create <name>                           # Create a repository group
gitnexus group add <group> <groupPath> <registryName>  # Add a repo. <groupPath> is a hierarchy path
                                                       # (e.g. hr/hiring/backend); <registryName> is the
                                                       # repo's name from the registry (see `gitnexus list`)
gitnexus group remove <group> <groupPath>              # Remove a repo by its hierarchy path
gitnexus group list [name]                             # List groups, or show one group's config
gitnexus group sync <name>                             # Extract contracts and match across repos/services
gitnexus group contracts <name>                        # Inspect extracted contracts and cross-links
gitnexus group query <name> <q>                        # Search execution flows across all repos in a group
gitnexus group status <name>                           # Check staleness of repos in a group
gitnexus group impact <name> --target <symbol> --repo <groupPath>  # Cross-repo blast radius

Project config (.gitnexusrc)

Commit a .gitnexusrc JSON file at the repo root to preconfigure recurring analyze options per project, instead of re-passing the same flags every run. It is read from the resolved repo root (not .gitnexus/, which is gitignored index storage). CLI flags always override .gitnexusrc.

jsonc
{
  // Default branch used in the generated regression-compare example (base_ref).
  // Use this so a project on `develop`/`master` doesn't get "main" rewritten
  // over its fix on every analyze. (Alias: "branch".)
  "defaultBranch": "develop",
  "skipContextFiles": true, // alias of skipAgentsMd: keep your own AGENTS.md/CLAUDE.md
  "skipSkills": true, // don't install standard skill files under .claude/skills/ and .agents/skills/
  "embeddings": true, // generate embeddings by default
  "springActuator": "./actuator", // optional local runtime snapshot directory or bundle
  "workerTimeout": 60,
}

A nested analyze block is also accepted (and overrides flat keys for the same option):

json
{ "analyze": { "defaultBranch": "develop", "skipSkills": true } }

Notes:

  • The default branch is resolved as: --default-branch > .gitnexusrc defaultBranch/branch > auto-detected origin/HEAD > main.
  • skipContextFiles / skipAiContext are aliases for skipAgentsMd โ€” they skip the AGENTS.md / CLAUDE.md block only. They do not imply skipSkills. indexOnly is the stronger option that skips all file injection.
  • Supported keys: defaultBranch (branch), skipAgentsMd (skipContextFiles, skipAiContext), skipSkills, indexOnly, stats/noStats, embeddings, dropEmbeddings, name, allowDuplicateName, maxFileSize, workerTimeout, walCheckpointThreshold, workers, maxProcesses, maxProcessBranching, maxProcessTraceDepth, maxEntryPointCandidates, springActuator, embeddingThreads, embeddingBatchSize, embeddingSubBatchSize, embeddingDevice.
  • The file is JSON only. Unknown keys and wrong JSON types fail fast with an actionable error before analysis starts. Process-detection knobs (maxProcesses, maxProcessBranching, maxProcessTraceDepth, maxEntryPointCandidates) that are not a positive integer warn and fall through to env, then the built-in default.

Environment variables

Most analyze knobs are also CLI flags (--workers, --worker-timeout, --max-file-size, --verbose). Use the env-var form when you'd otherwise repeat the same flag every run, or when invoking GitNexus from a long-running host (MCP server, eval-server, CI shell) that already manages its own environment. CLI flags take precedence over .gitnexusrc, which takes precedence over env vars, which take precedence over built-in defaults.

Variable Default Effect Tune whenโ€ฆ
GITNEXUS_TEST_REPORT unset Write a JSON execution receipt from scripts/run-cross-platform.ts; accepts a basename ending in .json. CI platform shards use distinct filenames so the completeness check can require every report.
GITNEXUS_WORKER_POOL_SIZE cores - 1, capped at 16 Parse worker pool size (must be โ‰ฅ 1). Equivalent to --workers <n>. The worker pool is the sole parse path โ€” there is no sequential parser, so 0 is rejected with an actionable error (the pool self-heals via quarantine + respawn). Constrained containers (cgroup CPU limits) or CI runners with explicit quotas. To narrow down a worker crash set 1 for a single-worker pool โ€” not 0.
GITNEXUS_PARSE_CHUNK_CONCURRENCY 2 Number of chunks whose file contents may be read into memory in parallel while the pool dispatches the current chunk. Worker dispatch itself stays serial. Repos large enough to chunk (multi-MB total source) where disk I/O is a measurable fraction of analyze wall-clock.
GITNEXUS_VERBOSE unset When 1, enables verbose ingestion logs (skipped-file warnings, per-chunk throughput, parse-cache stats). Equivalent to --verbose. Debugging an analyze that "completed" but seems to have missed files; tuning --workers / chunk concurrency against observable throughput.
GITNEXUS_EMBEDDING_RETRY_TIMEOUTS unset When truthy (1/true/yes), per-attempt HTTP embedding timeouts (TimeoutError on fetch or body read) go through the bounded GITNEXUS_EMBEDDING_MAX_ATTEMPTS retry loop instead of failing the job. Any other value leaves it off, so cloud/default timeouts remain terminal. Local accelerators that drop a device lock when the client disconnects and succeed on the next request (observed with FastFlowLM on Ryzen AI).
GITNEXUS_EMBEDDING_SIDECAR_TIMEOUT_MS 180000 (3 minutes) Per-request IPC timeout for local embedding sidecar embed batches. On overrun the parent SIGKILLs the sidecar child and rejects the batch. Init still uses the HF download budget (HF_DOWNLOAD_TIMEOUT_MS ร— attempts), not this knob. Large embed batches or slow local ONNX inference cause sidecar request timeouts during analyze --embeddings, embeddings sync, serve, or MCP.
GITNEXUS_VECTOR_MAX_DISTANCE 0.6 for CLI/MCP query; 0.5 for standalone semantic search Maximum cosine distance for vector hits, applied to both indexed search and exact-scan fallback. Hits must have distance < cutoff. Unset/blank uses the default; non-numeric, non-finite, zero, or negative values warn and use the default; finite values above 2 warn and clamp to 2. Paraphrased queries have weak semantic recall with your embedding model. See Vector cutoff tuning.
GITNEXUS_ANALYZER_IDENTITY_IN_PROCESS_GUARDS unset When truthy (1/true/yes), forces in-process cache-guard validation once a batch has โ‰ฅ128 requests. In-process mode also auto-selects when packageRoot/buildRoot fail W_OK with EACCES/EROFS. Otherwise those large batches use a Node subprocess probe. Batches under 128 always stay in-process. Trusted or read-only installs where two identity subprocess spawns per analyze dominate wall time; leave unset to keep the default isolation path on writable trees.
GITNEXUS_RESOLVE_DEF_GRAPH_ID_MEMO on (unset) Memoizes resolveDefGraphId per nodeLookup instance (WeakMap). Enabled by default. Set to 0/false/off/no to disable and recompute on every call (debug / bisect memo bugs). Suspecting stale graph-id resolution after a lookup rebuild, or comparing memo vs uncached cost on a large index.
GITNEXUS_AUTH_TOKEN unset Bearer token required when eval-server binds beyond loopback. May also be read from .env.local or .env; shell values take precedence. Exposing the evaluation HTTP tools to a container, VM, or LAN.
GITNEXUS_MCP_AUTH_TOKEN unset Bearer token for the dedicated gitnexus mcp --http server, for a directly reachable gitnexus serve /api/mcp route, and for the docker-server / web proxy in front of one. A non-loopback dedicated MCP bind requires it; serve enables protocol-layer MCP auth when it is set. Behind a proxy, set the same value on both services: the proxy spends the edge GITNEXUS_SERVE_AUTH_TOKEN, then replaces Authorization with this token on /api/mcp only. Dedicated MCP, a serve the client can reach directly, or a proxied deploy (Render Blueprint) where the backend runs protocol-layer MCP auth โ€” configure it on the proxy too.
GITNEXUS_PROFILE_DEFERRED unset When 1, emits [deferred-profile] timing/progress logs for the post-chunk deferred resolution band (imports โ†’ heritage โ†’ buildHeritageMap โ†’ legacy call resolution). Implied by GITNEXUS_VERBOSE. Diagnosing analyze stalls in "Resolving calls (all chunks)" on large Java/Kotlin repos (issue #1741) without the full verbose ingestion noise.
GITNEXUS_PROFILE_DEFERRED_SLOW_MS 3000 (verbose) / 5000 Per-file threshold in ms above which processCallsFromExtracted emits a slow file โ€ฆ log line. Parsed via Number(): accepts integers (5000), scientific notation (2.5e3), decimals (.5), and hex (0x10). Non-finite or non-positive values fall back to the default. Hunting a few outlier files dominating the deferred call-resolution stage; lower to surface more, raise to focus only on the worst.
PROF_LBUG_LOAD unset When 1, emits one [lbug-load prof] summary line per loadGraphToLbug call breaking the graph-DB persistence wall into stages (csv-emit / copy-nodes / copy-rels / fallback / total) plus node & edge counts. Zero-cost when unset. Attributing large-repo analyze wall time across CSV generation vs. LadybugDB COPY (issue #2203) โ€” the analyze "emit" timing is the scope-resolution bucket, not this DB-write path.
GITNEXUS_MAX_FILE_SIZE 512 (KB) Walker skip threshold in KB. Hard cap is 32768 (tree-sitter buffer ceiling). Equivalent to --max-file-size <kb>. Indexing repos with intentionally-large source files (generated parsers, vendored bundles) that should still be parsed.
GITNEXUS_MAX_PROCESSES dynamic (max(20, round(symbols/10))) Analyze-time process-detection process cap. Equivalent to --max-processes <n> / .gitnexusrc maxProcesses. Explicit values replace the dynamic formula (not a multiplier). 0 is invalid, not unlimited. Changing this re-detects flows on the next analyze without --force. Distinct from query-time IMPACT_MAX_CHUNKS. [processes] โ€ฆ whole flows are MISSING names --max-processes after entry points were never traced or flows were dropped. Tracing does not start the next entry once collected traces already reach maxProcesses * 2; a started entry can still emit every trace that entry produces.
GITNEXUS_MAX_PROCESS_BRANCHING 4 Analyze-time per-node branching cap during flow tracing. Equivalent to --max-process-branching <n>. Shape-only: raising it shortens fewer traces; it does not restore whole missing flows. A flow is present but calleesDropped is high at debug.
GITNEXUS_MAX_PROCESS_TRACE_DEPTH 10 Analyze-time DFS depth cap during flow tracing. Equivalent to --max-process-trace-depth <n>. Shape-only. A reported flow is shorter than the code path (tracesDepthCapped at debug).
GITNEXUS_MAX_ENTRY_POINT_CANDIDATES 200 Ranked entry-point candidate pool. Equivalent to --max-entry-point-candidates <n>. Raising --max-processes alone does not clear entryPointCandidatesDropped. Doubling the current cap is the usual first raise; setting it to the full remaining candidate count can exhaust CPU and memory. The [processes] warning reports candidate entry points that never ranked in.
GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS 30000 Worker idle timeout in milliseconds before retry/fallback. Equivalent to --worker-timeout <seconds> ร— 1000. Slow-parsing files (large minified JS, deeply-nested TS types) that legitimately need more than 30s.
GITNEXUS_WORKER_READY_TIMEOUT_MS 5000 Startup budget in milliseconds for a parse worker to load its grammar bindings and report {type:'ready'}. Slots that miss it are treated as startup crashes. Slow or heavily loaded hosts where a full pool cold-starting concurrently needs more than 5s, and analyze aborts with "did not report ready within 5000ms".
GITNEXUS_FTS_STEMMER porter Stemmer used when rebuilding BM25/FTS indexes. Use none for CJK-heavy repositories, or a language stemmer such as german, french, or spanish for matching repository comments. Re-run gitnexus analyze --repair-fts after changing it. Keyword search quality is poor for non-English comments or identifiers under English stemming.
GITNEXUS_STORAGE_PATH unset (<repo>/.gitnexus/) Complete external index directory. This preserves the existing configuration semantics and takes precedence over GITNEXUS_STORAGE_ROOT when both are set. You already keep one repository index outside its checkout or need one explicit index location.
GITNEXUS_STORAGE_ROOT unset Absolute root directory for external indexes. GitNexus creates an isolated <repo-basename>-<canonical-path-hash>/ slot beneath it for each repository, then registers the resolved slot so status, MCP, and serve can reopen it later. You want to manage multiple repository indexes centrally or keep generated data outside source checkouts.
GITNEXUS_SHARED_STORE unset (on) Set to off (or 0, false, no) to turn off shared index stores for both linked git worktrees and sibling clones; every checkout then indexes into its own .gitnexus/. Sharing is also off whenever GITNEXUS_STORAGE_PATH or GITNEXUS_STORAGE_ROOT is set. Disk or memory is not a concern, or you want each worktree's index fully independent.
GITNEXUS_CONTENT_RETENTION full Source-text retention profile: full keeps file and symbol text, symbol keeps symbol snippets without full file content, and none keeps the structural graph without source body text. You need to reduce persisted source text while preserving graph structure.
GITNEXUS_SKIP_FTS unset When exactly 1, skips FTS extension loading and keyword index creation during analyze. Equivalent to --skip-fts; a later analyze without either option restores FTS. Graph-only consumers with their own retrieval, or short-lived indexes that do not need keyword search.
GITNEXUS_WAL_CHECKPOINT_THRESHOLD 67108864 (64 MiB) LadybugDB WAL auto-checkpoint threshold in bytes. Equivalent to --wal-checkpoint-threshold . -1 keeps LadybugDB's stock threshold (~16 MiB). Larger thresholds reduce checkpoint frequency but increase the WAL size at rotation time โ€” choose a smaller value on disk-constrained environments. You need a larger or smaller WAL auto-checkpoint threshold for your analyze workload.
GITNEXUS_LBUG_BUFFER_POOL_SIZE min(2 GiB, 80% RAM) LadybugDB buffer-pool ceiling in bytes for every GitNexus database (analyze, MCP server, serve, group bridges). 0 restores LadybugDB's native unbounded default of 80% of system RAM; invalid values warn and fall back to the default (#2557). During analyze the pool is right-sized to the graph, scaled on non-4 KiB-page hosts by the page-size granule ratio up to min(2 GiB ร— pageSize/4 KiB, 80% RAM) (#2631); this env var overrides all of that as an absolute value. A long-lived gitnexus mcp or a big incremental analyze uses too much memory, or a huge repo's working set genuinely needs a pool larger than 2 GiB.
GITNEXUS_LBUG_MAX_DB_SIZE 17179869184 (16 GiB) Maximum size in bytes of a single LadybugDB database file โ€” an mmap/disk-address-space ceiling, not a memory limit (it does not constrain the buffer pool). Invalid values silently fall back to the default. Indexing a genuinely huge monorepo whose on-disk graph index approaches 16 GiB.
GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES 8388608 (8 MB) Per-job byte budget the pool will send to a worker in one postMessage. Very large individual files; mostly diagnostic โ€” bumping past 8 MB risks structured-clone memory pressure.
GITNEXUS_WORKER_MAX_RESPAWNS_PER_SLOT 3 Max replacement spawns per worker slot before the slot is dropped from the active rotation. Bounds respawn loops on a chronically-crashing slot. Hosts where a flaky worker should retry more (raise) or fail-fast (lower) before the slot is dropped.
GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS 5 ร— subBatchTimeoutMs Total retry wall-time budget per job before quarantining. Combined with timeoutBackoffFactor, prevents exponentially-growing retries from stalling for hours. Slow files that legitimately need long total retry windows; lower to fail-fast on stalls.
GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD max(3, poolSize) Per-slot consecutive deaths before the pool's circuit breaker trips. After tripping, every subsequent dispatch rejects until a fresh pool is created. Hosts where a SIGSEGV-prone native grammar should trip the breaker sooner; CI runners that should fail loudly.
GITNEXUS_WORKER_SHUTDOWN_DRAIN_MS 30000 Max wait at pool shutdown for a retired worker still inside native code. The worker is terminated at its next JS-safe point instead of mid-native-call (which aborts the whole process with Napi::Error, #2432); on expiry it is left running, unref'd, and terminated when it surfaces. Shutdown latency matters more than draining a wedged worker (lower), or a legitimately-slow native grammar needs longer to surface (raise).
GITNEXUS_CPP_CAPTURE_BUDGET_MS 20000 Per-file wall-clock budget for C++ capture extraction. On breach the file keeps the captures accumulated so far and logs a warning โ€” the worker returns to JS instead of stalling in native-heavy loops (#2432). 0 expires immediately. Pathological generated C++ that still exceeds the budget after the indexed lookups; raise for completeness, lower to fail-fast.
GITNEXUS_CHUNK_BYTE_BUDGET 2097152 (2 MB) Per-bucket byte budget for parse-cache packing. Files are grouped by (language, hash(path) mod 128); packs inside a bucket are cut at this limit. Smaller = finer-grained invalidation and more dispatch. Default is always 2 MiB and no longer scales with worker count. Tuning incremental-analyze cache invalidation on monorepos without changing --workers.
GITNEXUS_NO_GITIGNORE unset When set, skips .gitignore parsing. .gitnexusignore is still honored. Indexing a repo whose .gitignore excludes files you actually want indexed (e.g., generated code committed for cross-repo lookup).
GITNEXUS_SKIP_OPTIONAL_GRAMMARS unset When =1 strictly, skips the vendored grammar materialize for tree-sitter-dart, tree-sitter-proto, tree-sitter-swift, and tree-sitter-kotlin at install time (and the Dart/Proto source builds). Those four won't be parsed; the install still succeeds. Installing on a host without a C++ toolchain or where the vendored prebuilds don't match; willing to skip Dart/Proto/Swift/Kotlin parsing.
GITNEXUS_MCP_READ_ONLY unset Set to 1 to expose only proven single-repository read tools and resources; 0 disables the policy and any other value fails startup. The MCP server runs in an environment where graph mutation, raw Cypher, and cross-repository group routing must be unavailable.
GITNEXUS_MCP_ALLOWED_REPOS unset Comma-separated allowlist of canonical indexed repository names or absolute paths. Invalid, ambiguous, or blank entries fail startup. One MCP process must expose only a bounded subset of the repositories in the global registry.
GITNEXUS_MCP_DEFAULT_REPO unset Canonical indexed repository name or absolute path used when a tool or resource omits its repository. Must belong to the allowlist when one is set. Several repositories are available but unqualified MCP calls should resolve deterministically.
GITNEXUS_MCP_DEFAULT_MAX_TOKENS unset Default positive-integer response budget for MCP query, context, and impact, estimated at four UTF-8 bytes per token. Explicit maxTokens wins. Long MCP responses consume too much model context and callers cannot reliably add a per-request budget.
GITNEXUS_PUBLIC_ORIGIN unset The single browser origin serve is reached through, added to the CORS allowlist and to the write-route origin guard. A wildcard bind (0.0.0.0) has no host identity, so without this the server's own UI is refused. Setting it currently refuses to start: serve has no authentication, requests carrying no Origin header already reach POST /api/analyze and DELETE /api/repo, and this is the setting that would admit browser writes on top of that. Matching rules for when the gate lifts: the hostname must match exactly, and so must the scheme. A value with no scheme (app.example.com) means https, since a bare host comes from platform service discovery and those terminate TLS; spell out http://app.example.com for plain HTTP. An explicit port must match; with no port, any port on that hostname is accepted. Anything that is not one reachable host (a list, *, a bare port number, a :0 port, a trailing dot) warns at startup and allows nothing. gitnexus serve runs behind a reverse proxy or on a wildcard bind, and the UI's index/delete requests return origin_not_allowed.
GITNEXUS_TRUST_PROXY loopback, linklocal, uniquelocal Express trust proxy value โ€” which upstream hops may set X-Forwarded-*, and so what the per-IP rate limiter reads as the client IP. Set it to the exact number of proxies you control. Every hop past that is one more entry of the chain the caller gets to write. false/no/off (and a 0 hop count) trust no hop; a proxy list Express can compile (loopback, 10.0.0.0/8, 127.0.0.1) names them instead. true/yes/on is rejected: it reads the client-controlled leftmost X-Forwarded-For entry, so a spoofed chain earns a fresh rate-limit key per request, and express-rate-limit rejects it too (ERR_ERL_PERMISSIVE_TRUST_PROXY). Counts above 16 are rejected as well, as a sanity ceiling rather than a safety boundary. Any invalid value warns and falls back to the default. Bind non-loopback with this unset and serve warns: a load balancer outside the private ranges is untrusted, so every request keys to the balancer and the per-IP limit becomes one shared limit. serve sits behind a load balancer outside the private ranges (AWS ALB, Cloudflare, CGNAT), where every request otherwise collapses to the proxy hop and rate limiting goes global.

Vector cutoff tuning

GITNEXUS_VECTOR_MAX_DISTANCE controls which vector candidates enter hybrid search. Cosine distance is lower for more similar vectors; the appropriate cutoff depends on the embedding model and repository. A higher cutoff can recover useful semantic matches, but can also admit less relevant hits and change the fused ranking. The default is 0.6 for CLI and MCP query, and 0.5 for the standalone semantic-search function.

For an index that already has embeddings, compare the default with 0.8 and 1.0 on representative queries, including paraphrases, exact symbol names, and queries that should have no relevant result:

bash
GITNEXUS_VECTOR_MAX_DISTANCE=0.8 gitnexus query "how are expired sessions removed" --limit 10

Check both whether the expected symbols appear and where they rank. In #3457, the reporter's voyage-code-4 sample found 20 of 32 paraphrased targets at 0.6, 25 at 0.8, and 28 at 1.0. Those values are a tuning starting point for that model, not universal recommendations: one paraphrased target fell from first to twelfth at 1.0. The sample covered eight repositories, recorded no raw distances, used agent-written queries after reading the code, and did not send Voyage's input_type.

Set the variable in the process that runs the search. For MCP, add it to the server's launch environment and restart the server; for gitnexus serve, set it in that process's environment and restart it. Setting it only during analyze, or exporting it in another shell, does not configure an existing server. Changing the cutoff requires no reindex.

The cutoff only filters available vector candidates. It cannot add missing embeddings or repair a mismatch between the indexing and query-time embedding configuration; see the embeddings runbook. A cutoff of 2 is very permissive, but still rejects distance exactly 2 and retains the search candidate limits.

gitnexus uninstall

gitnexus uninstall reverses gitnexus setup โ€” it removes the GitNexus MCP entries, hooks, and skill directories it added to each detected editor. Skill directories are identified by bundled gitnexus skill name (e.g. gitnexus-cli/), so if you customized files inside an installed skill directory, back them up first. It is a dry-run preview by default and prints the exact paths it would remove; pass --force to apply. Per-repo indexes (gitnexus clean --all) and the global npm package (npm uninstall -g gitnexus) are left for you to remove.

Publishing to understand-quickly (opt-in)

looptech-ai/understand-quickly is a public registry of code-knowledge graphs that lists gitnexus@1 as a first-class format. After registering your repo once (npx @understand-quickly/cli add or the wizard), gitnexus publish fires a single repository_dispatch event so the registry resyncs your entry on demand instead of waiting for the nightly job.

It is opt-in and a no-op without UNDERSTAND_QUICKLY_TOKEN โ€” a fine-grained GitHub PAT with Repository dispatches: write on the registry repo. Nothing else happens; no graph file is uploaded. See the protocol spec for the full contract.

How It Works

GitNexus builds a complete knowledge graph of your codebase through a multi-phase indexing pipeline:

  1. Structure โ€” walks the file tree and maps folder/file relationships
  2. Parsing โ€” extracts functions, classes, methods, and interfaces using Tree-sitter ASTs
  3. Resolution โ€” resolves imports, function calls, heritage, constructor inference, and self/this receiver types across files with language-aware logic
  4. Clustering โ€” groups related symbols into functional communities
  5. Processes โ€” traces execution flows from entry points through call chains
  6. Search โ€” builds hybrid search indexes for fast retrieval

Supported Languages

Language Imports Named Bindings Exports Heritage Type Annotations Constructor Inference Config Frameworks Entry Points
TypeScript โœ“ โœ“ โœ“ โœ“ โœ“ โœ“ โœ“ โœ“ โœ“
JavaScript โœ“ โœ“ โœ“ โœ“ โ€” โœ“ โœ“ โœ“ โœ“
Python โœ“ โœ“ โœ“ โœ“ โœ“ โœ“ โœ“ โœ“ โœ“
Java โœ“ โœ“ โœ“ โœ“ โœ“ โœ“ โ€” โœ“ โœ“
Kotlin โœ“ โœ“ โœ“ โœ“ โœ“ โœ“ โ€” โœ“ โœ“
C# โœ“ โœ“ โœ“ โœ“ โœ“ โœ“ โœ“ โœ“ โœ“
Go โœ“ โ€” โœ“ โœ“ โœ“ โœ“ โœ“ โœ“ โœ“
Rust โœ“ โœ“ โœ“ โœ“ โœ“ โœ“ โœ“ โœ“ โœ“
PHP โœ“ โœ“ โœ“ โ€” โœ“ โœ“ โœ“ โœ“ โœ“
Ruby โœ“ โ€” โœ“ โœ“ โ€” โœ“ โ€” โœ“ โœ“
Swift โ€” โ€” โœ“ โœ“ โœ“ โœ“ โœ“ โœ“ โœ“
C โ€” โ€” โœ“ โ€” โœ“ โœ“ โ€” โœ“ โœ“
C++ โ€” โ€” โœ“ โœ“ โœ“ โœ“ โ€” โœ“ โœ“
Objective-C โœ“ โ€” โœ“ โœ“ โœ“ โ€” โ€” โ€” โ€”
Dart โœ“ โ€” โœ“ โœ“ โœ“ โœ“ โ€” โœ“ โœ“
Zig โœ“ โ€” โœ“ โ€” โœ“ โœ“ โœ“ โ€” โœ“

Imports โ€” cross-file import resolution ยท Named Bindings โ€” import { X as Y } / re-export tracking ยท Exports โ€” public/exported symbol detection ยท Heritage โ€” class inheritance, interfaces, mixins ยท Type Annotations โ€” explicit type extraction for receiver resolution ยท Constructor Inference โ€” infer receiver type from constructor calls (self/this resolution included for all languages) ยท Config โ€” language toolchain config parsing (tsconfig, go.mod, etc.) ยท Frameworks โ€” AST-based framework pattern detection ยท Entry Points โ€” entry point scoring heuristics

Control flow (CFG, opt-in --pdg) โ€” per-function control-flow graphs (BasicBlock nodes + CFG edges) feeding the PDG/taint substrate, currently TypeScript & JavaScript (#2081 M1); other languages planned. Off by default.

Multi-Repo Architecture

GitNexus uses a global registry so one MCP server can serve multiple indexed repos. No per-project MCP config needed โ€” set it up once and it works everywhere.

Each gitnexus analyze stores the index in .gitnexus/ inside the repo by default (portable, gitignored). GITNEXUS_STORAGE_PATH selects one complete external index directory and preserves the established configuration behavior. To manage multiple repositories under one external directory, set GITNEXUS_STORAGE_ROOT; GitNexus derives an isolated <repo-basename>-<canonical-path-hash>/ slot beneath it for each repository. If both variables are set, GITNEXUS_STORAGE_PATH takes precedence. GitNexus registers the resolved slot in ~/.gitnexus/registry.json, allowing later status, MCP, and serve commands to reopen the index without repeating the environment variable. LadybugDB connections are opened lazily on first query and evicted after 5 minutes of inactivity (max 5 concurrent). Read-only tools can omit repo when only one repo is indexed, an MCP default is configured, or the GitNexus process cwd is inside a registered path without crossing into an unindexed nested Git checkout. Outside those pathsโ€”and for mutating tools with multiple indexed repos and no MCP defaultโ€”pass repo explicitly.

Worktrees share one index store. When a repository has linked worktrees (git worktree add), the main checkout and every worktree index into one store at ~/.gitnexus/stores/<repo>/ instead of each keeping a full .gitnexus/. Checkouts at the same commit with no local changes read one shared, read-only graph: one copy on disk and one open database in MCP. A checkout with uncommitted changes gets its own graph, copied from the nearest shared graph and updated incrementally rather than rebuilt. Parse caches are shared too. Each worktree keeps a small .gitnexus/store.json pointer, and an index it had before sharing is left in place; gitnexus status reports it and gitnexus clean --local-index --force removes it. gitnexus clean in one worktree removes only that worktree's slot and any shared graph no other checkout uses; gitnexus clean --gc also drops slots whose worktree was deleted. Independent clones of one repository share too: when another registered clone has the same origin URL, gitnexus analyze in a clone joins that clone's store (or starts one the other clone joins on its next analyze). A lone clone keeps its own .gitnexus/. gitnexus analyze --share-with <name-or-path> joins a specific checkout's store after checking the origin URLs match, and --no-share moves a clone back to its own .gitnexus/ and keeps it out until --share-with. On filesystems with copy-on-write clones (APFS, btrfs, XFS) a checkout's private graph shares its unchanged pages with the shared graph on disk; elsewhere it is a full copy, and gitnexus status says which. Queries cannot combine two graphs, because LadybugDB reads one database per query, so a checkout with edits always has a complete graph of its own. Set GITNEXUS_SHARED_STORE=off (or 0, false, no) to turn sharing off for worktrees and clones alike.

Architecture diagram

mermaid
flowchart TD
    subgraph CLI [CLI Commands]
        Setup["gitnexus setup"]
        Analyze["gitnexus analyze"]
        Clean["gitnexus clean"]
        List["gitnexus list"]
    end

    subgraph Registry ["~/.gitnexus/"]
        RegFile["registry.json"]
    end

    subgraph Repos [Project Repos]
        RepoA[".gitnexus/ in repo A"]
        RepoB[".gitnexus/ in repo B"]
    end

    subgraph MCP [MCP Server]
        Server["server.ts"]
        Backend["LocalBackend"]
        Pool["Connection Pool"]
        ConnA["LadybugDB conn A"]
        ConnB["LadybugDB conn B"]
    end

    Setup -->|"writes global MCP config"| CursorConfig["~/.cursor/mcp.json"]
    Analyze -->|"registers repo"| RegFile
    Analyze -->|"stores index"| RepoA
    Clean -->|"unregisters repo"| RegFile
    List -->|"reads"| RegFile
    Server -->|"reads registry"| RegFile
    Server --> Backend
    Backend --> Pool
    Pool -->|"lazy open"| ConnA
    Pool -->|"lazy open"| ConnB
    ConnA -->|"queries"| RepoA
    ConnB -->|"queries"| RepoB

Tool Examples

Impact Analysis

text
impact({target: "UserService", direction: "upstream", minConfidence: 0.8})

TARGET: Class UserService (src/services/user.ts)

UPSTREAM (what depends on this):
  Depth 1 (WILL BREAK):
    handleLogin [CALLS 90%] -> src/api/auth.ts:45
    handleRegister [CALLS 90%] -> src/api/auth.ts:78
    UserController [CALLS 85%] -> src/controllers/user.ts:12
  Depth 2 (LIKELY AFFECTED):
    authRouter [IMPORTS] -> src/routes/auth.ts

Options: maxDepth, minConfidence, relationTypes (CALLS, IMPORTS, EXTENDS, IMPLEMENTS), includeTests, limit (max symbols per depth, default 100), offset (pagination start per depth), summaryOnly (counts and risk only, omits symbol list)

Disambiguation โ€” when several symbols share the target name, impact returns a ranked ambiguous candidate list instead of guessing. Narrow it with target_uid (exact, zero-ambiguity), file_path, or kind (Function, Class, Method, โ€ฆ). From the CLI these are --uid, --file, and --kind, matching gitnexus context:

bash
gitnexus impact get_embeddings                       # โ†’ ambiguous: lists ranked candidates
gitnexus impact get_embeddings --file src/embed.py   # โ†’ resolves to the one in that file
gitnexus impact get_embeddings --uid "Function:src/embed.py:get_embeddings"  # exact

More examples: search ยท context ยท detect_changes ยท rename ยท Cypher

text
query({search_query: "authentication middleware"})

processes:
  - summary: "LoginFlow"
    priority: 0.042
    symbol_count: 4
    process_type: cross_community
    step_count: 7

process_symbols:
  - name: validateUser
    type: Function
    filePath: src/auth/validate.ts
    process_id: proc_login
    step_index: 2

definitions:
  - name: AuthConfig
    type: Interface
    filePath: src/types/auth.ts

Context (360-degree Symbol View)

text
context({name: "validateUser"})

symbol:
  uid: "Function:validateUser"
  kind: Function
  filePath: src/auth/validate.ts
  startLine: 15

incoming:
  calls: [handleLogin, handleRegister, UserController]
  imports: [authRouter]

outgoing:
  calls: [checkPassword, createSession]

processes:
  - name: LoginFlow (step 2/7)
  - name: RegistrationFlow (step 3/5)

Detect Changes (Pre-Commit)

text
detect_changes({scope: "all"})

summary:
  changed_count: 12
  affected_count: 3
  changed_files: 4
  risk_level: medium

changed_symbols: [validateUser, AuthService, ...]
affected_processes: [LoginFlow, RegistrationFlow, ...]

Rename (Multi-File)

text
rename({symbol_name: "validateUser", new_name: "verifyUser", dry_run: true})

status: success
files_affected: 5
total_edits: 8
graph_edits: 6     (high confidence)
text_search_edits: 2  (review carefully)
changes: [...]

Cypher Queries

cypher
-- Find what calls auth functions with high confidence
MATCH (c:Community {heuristicLabel: 'Authentication'})<-[:CodeRelation {type: 'MEMBER_OF'}]-(fn)
MATCH (caller)-[r:CodeRelation {type: 'CALLS'}]->(fn)
WHERE r.confidence > 0.8
RETURN caller.name, fn.name, r.confidence
ORDER BY r.confidence DESC

Wiki Generation

Generate LLM-powered documentation from your knowledge graph:

bash
# Requires an LLM API key (OPENAI_API_KEY, etc.)
gitnexus wiki

# Use a custom model or provider (default model: minimax/minimax-m2.5)
gitnexus wiki --model gpt-4o
gitnexus wiki --base-url https://api.anthropic.com/v1
gitnexus wiki --provider grok   # local Grok Build CLI (uses `grok login`, no API key)

# Force full regeneration
gitnexus wiki --force

# Increase the timeout or retries for large codebases or slow LLM providers
gitnexus wiki --timeout <seconds>  # LLM request timeout in seconds (default: disabled)
gitnexus wiki --retries <n>        # Max LLM retry attempts per request (default: 3)

# Allow a specific LAN/self-hosted HTTP LLM host (HTTPS is preferred for remote endpoints)
gitnexus wiki --base-url http://llama-box.local:8080/v1 --allow-insecure-connection llama-box.local
# Or set a comma-separated host allowlist:
GITNEXUS_ALLOW_INSECURE_CONNECTION=llama-box.local,192.168.1.23

# Change the output language
gitnexus wiki --lang <lang>  # e.g. english, chinese, spanish, japanese

For safety, http:// LLM base URLs are allowed by default only for loopback hosts (localhost, 127.0.0.1, ::1). --allow-insecure-connection and GITNEXUS_ALLOW_INSECURE_CONNECTION accept exact hostnames or IP addresses only; do not include schemes, ports, paths, credentials, or wildcards.

The wiki generator reads the indexed graph structure, groups files into modules via LLM, generates per-module documentation pages, and creates an overview page โ€” all with cross-references to the knowledge graph.

Web UI (browser-based)

A client-side graph explorer and AI chat โ€” your code never leaves your machine.

Try it now: gitnexus.vercel.app โ€” run npx gitnexus@latest serve locally and the page auto-connects to your local backend.

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 โ€” it auto-detects the server and shows all your indexed repos, with full AI chat support. No re-upload, no re-index. The agent's tools (Cypher queries, search, code navigation) route through the backend HTTP API automatically.

Run the frontend locally

bash
git clone https://github.com/abhigyanpatwari/gitnexus.git
cd gitnexus/gitnexus-web && npm install
# Compile sibling gitnexus-shared with this package's TypeScript 7 (do not npm ci shared).
cd ../gitnexus-shared && node ../gitnexus-web/node_modules/typescript/lib/tsc.js
cd ../gitnexus-web && npm run dev
# Then in another terminal, start the backend the frontend connects to:
npx gitnexus@latest serve

Docker

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.

The official setup ships two signed images, published identically to GitHub Container Registry (GHCR) and Docker Hub โ€” same build, same digest, same Cosign signature:

Purpose GHCR (default in docker-compose.yaml) Docker Hub mirror
CLI / gitnexus serve backend (HTTP API on port 4747, MCP, indexer) ghcr.io/abhigyanpatwari/gitnexus:latest akonlabs/gitnexus:latest
Static web UI (port 4173) ghcr.io/abhigyanpatwari/gitnexus-web:latest akonlabs/gitnexus-web:latest

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

Heads-up โ€” image rename. Earlier releases published the web UI under ghcr.io/abhigyanpatwari/gitnexus. That slug now hosts the CLI/server image and the UI moved to ghcr.io/abhigyanpatwari/gitnexus-web. Previous tags remain pullable, but new versions are only published under the new slugs โ€” update your docker run / compose files (or just adopt the bundled compose).

Direct docker run & env file

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

Files:

  • Dockerfile.web โ€” builds gitnexus-shared and gitnexus-web, then serves the production frontend.
  • Dockerfile.cli โ€” builds the CLI/server (with its native deps) and runs gitnexus serve --host 0.0.0.0. Local embeddings are not in the image (onnxruntime-node is opt-in; runtime npm is stripped). Bind-mount a prefix or set GITNEXUS_EMBEDDING_URL.
  • docker-compose.yaml โ€” starts both signed images side by side.
  • .env.example โ€” overrides for image names, container names, ports, and the workspace mount.

Versioning & supply-chain protection (Cosign signatures, provenance, Kubernetes admission policy)

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 (and its Docker Hub mirror akonlabs/gitnexus:1.6.2) is byte-for-byte the same release as npm install [email protected] โ€” no drift, no floating builds from main. Both registries receive the same digest from a single build step, so you can pull from either and the signature verifies identically.
  • Release-candidate images (e.g. :1.7.0-rc.1) are published alongside each RC npm release. They are built by publish.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 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

# Same signature verifies the Docker Hub mirror (identical digest):
cosign verify docker.io/akonlabs/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. It is identical for both registries because both sets of tags were signed at the same digest in one workflow run.

Release candidates โ€” signed from refs/heads/main (the caller's ref when publish.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. Ship the bundled ClusterImagePolicy so the Sigstore 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.

Enterprise

GitNexus is available as an enterprise offering โ€” fully managed SaaS or self-hosted deployment. Commercial use of the OSS version is also available with proper licensing.

Enterprise includes:

  • PR Review โ€” automated blast radius analysis on pull requests
  • Auto-updating Code Wiki โ€” always up-to-date documentation (Code Wiki is also available in OSS)
  • Auto-reindexing โ€” knowledge graph stays fresh automatically
  • Multi-repo support โ€” unified graph across repositories
  • OCaml support โ€” additional language coverage
  • Priority feature/language support โ€” request new languages or features

Upcoming: auto regression forensics ยท end-to-end test generation

๐Ÿ‘‰ Learn more at akonlabs.com โ€” for commercial licensing or enterprise inquiries, ping us on Discord or email [email protected]

Community Integrations

Built by the community โ€” not officially maintained, but worth checking out.

Project Author Description
pi-gitnexus @tintinweb GitNexus plugin for pi โ€” pi install npm:pi-gitnexus
gitnexus-stable-ops @ShunsukeHayashi Stable ops & deployment workflows (Miyabi ecosystem)
KiloCode MCP workflow @oktanishq Guide to connect GitNexus MCP to Kilo Code and verify tools.

Have a project built on GitNexus? Open a PR to add it here!

Roadmap

Actively building:

  • LLM Cluster Enrichment โ€” semantic cluster names via LLM API
  • AST Decorator Detection โ€” parse @Controller, @Get, etc.
  • Incremental Indexing โ€” only re-index changed files

Recently completed:

  • Constructor-Inferred Type Resolution, self/this Receiver Mapping
  • Wiki Generation, Multi-File Rename, Git-Diff Impact Analysis
  • Process-Grouped Search, 360-Degree Context, Claude Code Hooks
  • Multi-Repo MCP, Zero-Config Setup, 14 Language Support
  • Community Detection, Process Detection, Confidence Scoring
  • Hybrid Search, Vector Index

Development

  • ARCHITECTURE.md โ€” packages, index โ†’ graph โ†’ MCP flow, where to change code
  • RUNBOOK.md โ€” analyze, embeddings, stale index, MCP recovery, CI snippets
  • GUARDRAILS.md โ€” safety rules and operational "Signs" for contributors and agents
  • CONTRIBUTING.md โ€” license, setup, commits, and pull requests
  • TESTING.md โ€” test commands for gitnexus and gitnexus-web

Tech Stack

Layer CLI Web
Runtime Node.js (native) Browser (WASM)
Parsing Tree-sitter native bindings Tree-sitter WASM
Database LadybugDB native LadybugDB WASM
Embeddings HuggingFace transformers.js (GPU/CPU) transformers.js (WebGPU/WASM)
Search BM25 + semantic + RRF BM25 + semantic + RRF
Agent Interface MCP (stdio) LangChain ReAct agent
Visualization โ€” Sigma.js + Graphology (WebGL)
Frontend โ€” React 18, TypeScript, Vite, Tailwind v4
Clustering Graphology Graphology
Concurrency Worker threads + async Web Workers + Comlink

Security & Privacy

  • CLI: everything runs locally on your machine. No network calls. Indexes are stored in .gitnexus/ by default (gitignored), in the complete external directory selected by GITNEXUS_STORAGE_PATH, or in repository-specific slots beneath GITNEXUS_STORAGE_ROOT. Global registry at ~/.gitnexus/ stores only paths and metadata.
  • Web: everything runs in your browser. No code uploaded to any server. API keys stored in localStorage only.
  • Open source โ€” audit the code yourself.

Star History

Acknowledgments


Documentation Kilo Code Mcp (`Documentation/kilo-code-mcp.md`)

Connect GitNexus to Kilo Code via MCP

This guide shows how to connect GitNexus to the Kilo Code VS Code extension using Kiloโ€™s MCP support, based on a setup that has been tested successfully.

Prerequisites

GitNexus should already be installed globally and working on the target repository, and the repository should be indexed successfully with gitnexus analyze before testing inside Kilo.

Tested Versions

Component Version
VS Code 1.125.1 (user setup)
Node.js 24.15.0
Kilo Code 7.3.50
OS Windows 11 25H2 / Windows_NT x64 10.0.26200
GitNexus 1.6.7

Where Kilo Stores MCP Config

Kilo Code stores MCP server configuration in its main config file. For the VS Code extension, config can be stored at either the global or project level.

Scope Config path
Global ~/.config/kilo/kilo.jsonc
Project kilo.jsonc or .kilo/kilo.jsonc in the project root

Check latest path : https://kilo.ai/docs/automate/mcp/using-in-kilo-code

Add GitNexus as an MCP Server

Kilo supports local MCP servers through STDIO, and GitNexus should be added as a local server under the mcp key in kilo.jsonc. Use this configuration:

jsonc
{
  "mcp": {
    "gitnexus": {
      "type": "local",
      "command": ["npx", "-y", "gitnexus@latest", "mcp"],
      "enabled": true,
      "timeout": 10000
    }
  }
}

Check It Through the Kilo UI

  1. restart kilo code extension or vs code
  2. open kilo code settings
  3. select mcp server section

From there, Kilo allows adding, editing, enabling, disabling, and deleting MCP servers, and it writes changes directly to the appropriate config file.

Test the Connection

After configuration, Kilo automatically detects the tools exposed by the MCP server and can use them from chat once the server is available.

A practical test flow is:

  1. Open the indexed repository in VS Code.
  2. Confirm gitnexus analyzecompleted successfully.
  3. Open Kilo chat and ask: Use GitNexus and explain What does index.php do?.
  4. Approve the MCP tool call if prompted.

Full Support will be added Soon ๐Ÿ˜Ž

Troubleshooting

  1. If the server shows failed, check the CLI output and confirm the command and paths are correct.
  2. If no tools appear, confirm the MCP server is enabled and GitNexus is exposing the expected tools.
  3. If Kilo does not automatically select GitNexus, note the exact settings you changed and mark them as an observed workaround.

Languages Jupyter Notebook (`docs/languages/jupyter-notebook.md`)

Jupyter notebook (.ipynb) indexing

Status: implemented (Python code cells)

GitNexus indexes Jupyter notebooks by extracting Python code cells and parsing them with the existing Python language provider. Notebooks are not executed.

Goal

After analyze, functions, classes, and imports defined in Python code cells are queryable like ordinary .py files.

Compatibility

  • .ipynb is detected as Python (gitnexus-shared EXTENSION_MAP and pythonProvider.extensions).
  • Extraction lives in gitnexus/src/core/ingestion/ipynb-extractor.ts. Shared ingestion modules do not name nbformat AST types.
  • Notebooks are not Python import targets. A notebook may import .py modules; import some_notebook does not resolve to an .ipynb.
  • Files over the walker size cap (default 512KB, GITNEXUS_MAX_FILE_SIZE) are skipped like any other oversized file. Output-heavy notebooks may need a higher cap. Outputs are not stripped in this slice.
  • Group-layer FastAPI/Flask/Django scanners that require a .py suffix still ignore notebooks.

Kernel and magics

  • Skip the file when kernelspec.language or language_info.name is present and is not a Python-family name (python, python2, python3, ipython, python 3, ipython3). python and python3 together still index.
  • If kernelspec.language is missing, kernelspec.name is used only when it is an obvious language id (python3, ir, julia-1.8). Conda env names are ignored.
  • If language_info.name is missing, language_info.file_extension (.py vs .r / .jl) is used the same way.
  • If those fields are absent, code cells are treated as Python unless a cell's own language, metadata.language, or metadata.vscode.languageId says otherwise.
  • A cell whose first non-empty line is a foreign cell magic (%%bash, %%html, %%sql, %%R) is skipped. Python-body cell magics (%%time, %%timeit, %%capture, %%prun, %%debug, %%px, %%python) stay, with the magic line commented.
  • %run, %load, and %loadpy of a local .py path become import module on that same line so the notebook links to the file. URLs, .. paths, and .ipynb targets stay comments. The notebook is not executed.
  • Sage, SageMath, MicroPython, Pyodide, PyPy, and PySpark kernels are indexed as Python. SQL, R, and Julia cells are not.
  • A cell with an unclosed string or bracket is commented out so it cannot hide later cells. Those cells are concatenated in order; one syntax error no longer drops the rest of the notebook.
  • Line magics (%), shell (!), and IPython help (train?, ?train) are commented in place so JSON line mapping stays affine.
  • nbformat v4 cells / source is the normal path. nbformat v3 worksheets[].cells and code-cell input are accepted. A leading UTF-8 BOM is accepted. Markdown and raw cells are not code.

Line numbers

Graph startLine / endLine are 0-based coordinates in the on-disk .ipynb JSON. FTS/MCP symbol snippets reconstruct cell Python; they do not slice raw JSON.

Concatenating cells in document order is notebook semantics. An earlier cell with a syntax error may cause later definitions to be missed; that is a documented limitation, not a per-cell fallback parse.

Tests

  • gitnexus/test/unit/ipynb-extractor.test.ts
  • gitnexus/test/unit/ingestion-utils.test.ts (.ipynb detection)
  • gitnexus/test/integration/resolvers/ipynb-python-pipeline.test.ts

No Jupyter, nbconvert, or nbformat runtime dependency.


Languages Local Import Scoping (`docs/languages/local-import-scoping.md`)

Local import scoping

GitNexus resolves statically known local imports through their lexical owner and
actual declaration identity. A nearer name that is unresolved, unavailable, or
noncallable blocks an unrelated outer or workspace fallback. An absent name can
continue through the existing lookup rules. This is static analysis: it does not
execute loaders or infer arbitrary runtime initialization order.

Shared contract

Language rules stay in providers. The shared
name-claim lookup
distinguishes absent, blocked, and resolved before filtering by target kind.
Its regressions
cover noncallable barriers, unresolved imports, type/value lookup, declaration
positions, and inferred aliases that retain their initializer's lookup position.
Calls, constructors, receiver/type inference, inheritance, namespace lookup, and
fallback consumers use these ownership facts rather than independently choosing a
same-named symbol.

The shared records carry
ownership, activation positions, directive destinations, and lookup purpose as
serializable data. They retain declaration identity and ambiguity; they contain
no AST nodes or callbacks. declaredAtScope describes ownership, while
runsOnlyWhenCalled describes deferred execution. A function-local Rust use or
C #include is not deferred merely because of its position; Python and Ruby
loading can be. The
function-local import chain tests
pin that distinction.

Providers with expanded lexical support

These seven entries are part of the
18-provider registry.
The evidence links cover exact targets and negative cases, not just edge counts.

Provider Supported behavior Source and regression evidence
Python Function-local named, aliased, and namespace imports; compiler-local ownership; ordered class lookup; global/nonlocal destinations. Lexical facts, local imports, namespace semantics
TypeScript Literal genuine require, immediately awaited import, and import-equals; namespace, named, and renamed bindings; type/value separation. Loader collector, loader tests, type ownership
JavaScript The same supported runtime loaders and lexical barriers, using JavaScript captures for JS/JSX. Captures, JS/JSX loader cases
Vue Local loader facts in extracted scripts, with original declaration graph positions preserved. Explicit JS/JSX uses its own semantic grammar inside the worker; TS reuses the cached tree. Script delegation, direct/pre-extracted parity, real workers
Rust Lexical use, producer identity for imported factories/types, and Cargo-aware crate/module identity. Unresolved explicit imports block unrelated fallback. Lexical facts, import targets, local scope, Cargo boundaries
C++ Position-sensitive local using declarations/directives, separate from includes; propagated header claims retain unresolved named using barriers. Captures, local using, header using
Zig Literal @import namespace/member bindings, block activation, container hoisting, stable deep aliases, and inline @import receivers. Captures, local imports, static gating

Python boundaries

Compiler-local binders include parameters, assignment patterns, loop/with/except
targets, match captures, augmented assignment, deletion, and comprehension/walrus
bindings. Comprehension targets do not leak, and the first iterable is evaluated
outside the comprehension environment. Class bodies use ordered locals; methods
skip class locals for bare-name lookup while explicit instance receivers remain
available. Annotation-only declarations distinguish compiler ownership from a
runtime store. These cases are characterized in the
Python local-import tests.

Mixed import/value rebinding and conflicting import identities conservatively
abstain throughout the environment, including simple sequential rebinding; this
is not a control-flow evaluator. A definite def/class declaration can still
supply its own target after activation. The
provider policy
and rebinding regressions
define this boundary. There is no full annotation evaluator, exec evaluation,
or general closure scheduling model. A deferred global/nonlocal installer is
not assumed to have executed for unrelated callers.

Conditional imports with one compatible identity remain available for structural
reference resolution, including TYPE_CHECKING annotations and try imports.
Their runtime execution is not guaranteed by a resolved graph edge.

Default expressions look up names in the enclosing environment. FastAPI's
synthetic dependency graph caller is recorded separately as callerScope; it
does not move lexical lookup into the handler body. Both
direct extraction
and worker transport
check this distinction.

JavaScript-family boundaries

Loader recognition checks AST shape and binding identity, including parameters,
catch/destructuring bindings, temporal-dead-zone ownership, and writes to the
loader or imported handle. CommonJS forwarding uses that same identity check.
Reassigning one destructured import leaves unchanged sibling re-exports intact.
Writes to ordinary locals retain their declared type and callable-value facts.
Unawaited dynamic imports remain Promise/file-dependency facts rather than module
namespace aliases. Computed specifiers, indirect loader/Promise chains, and
unsupported destructuring remain unresolved; see the
loader regressions.

Vue's explicit JS/JSX script requires an additional bounded semantic parse because
the worker's structural tree uses TypeScript. That parse runs inside the real
worker. The parity test
witnesses returned worker facts; a main-thread fallback does not satisfy it.

Rust, C++, and Zig boundaries

Rust uses Cargo editions, target roots, and module identities, including files
with multiple valid identities. Reusable per-analysis indexes bound repeated
lookup work; this is not a claim of a global indexing speedup. See
Cargo metadata
and its tests.
Exact same-name inline pub use forwarding is supported. A forwarding alias that
changes symbol spelling through a file-only target conservatively remains
unresolved; ambiguous same-file homonyms and re-export cycles cannot select an
unrelated declaration. These limits are explicit in the
Cargo fallback regressions.
Inferred receiver types retain their receiving declaration range. When same-block
shadowing replaces a retained type fact, earlier uses cannot consume the later
declaration's type and may remain unresolved; this is not full SSA inference. See
the factory ownership regressions.

C++ includes keep their compilation-unit behavior alongside lexical using.
The header tests cover literal includes, declaration positions, unresolved named
using claims, and overload identity. They do not establish a full preprocessor,
namespace-alias evaluator, or arbitrary transitive using namespace chain support.
Zig nonliteral imports remain unresolved; its block-local names do not escape or
activate before their declarations.

Retained provider behavior

The other eleven providers retain their existing import/loading models. This
work does not reinterpret every dependency construct as a local value import.

Provider Retained model and boundary Evidence
Ruby Wildcard loading; method-local loading is deferred. The extractor recognizes string arguments to require, require_relative, and load. The linked behavioral tests establish require/require_relative and deferral, not arbitrary load execution. Extractor, loading, deferral
PHP Namespace use imports are distinct from closure capture and trait use. The scope provider does not propagate names through include/require. Query, ownership, resolver coverage
C Header includes remain preprocessor/compilation-unit dependencies, including includes written inside functions; no full preprocessing is added. Include interpretation, include tests, deferral boundary
Objective-C Headers, modules, and compilation-unit siblings retain the dedicated provider rules. Provider guide, resolver, workspace tests
COBOL COPY stays on the dedicated copybook path with module ownership. Interpretation, COPY target parity
Go File imports and package sibling visibility retain separate provider rules. Import ownership, imports, package siblings
Java Imports belong to the compilation unit. Ownership, named/wildcard import resolution
Kotlin File import headers retain their existing alias/package resolution. Header decomposition, resolver tests
C# Namespace/file using directives remain imports; resource using statements are not import directives. Directive query, import tests
Swift Imports and target/module visibility retain provider ownership. Ownership, import decomposition, target siblings
Dart Library imports retain existing package identity and visibility rules. Ownership, library/package regressions

Taint provenance

Taint source, sink, and sanitizer matching uses the same lexical provenance.
A genuine external import may match its raw module name even when that package
has no indexed source. A sibling import, local shadow, or type-only import cannot
authorize that match. General copied-handle tracking is outside this support.
The taint regressions
cover extraction, emission, and function summaries, including abstention when
exact call-site positions are missing.

Cache migration and verification evidence

Parse-cache schema 136 replaces 133,
134, and 135. It invalidates overly broad Python/TS/JS binding barriers,
incorrect parameter-annotation lookup ownership, and embedded Vue grammar and
source positions. The same version gate applies to the
durable ParsedFile store.
Incompatible records are rejected and rebuilt on the next analysis; unchanged
records can then be reused. The
schema regression
explicitly rejects valid schema-133, schema-134, and schema-135 records in both stores.
PDG-mode chunk keys also advance to pdg:6, so embedded-script CFGs with
incorrect call anchors are rebuilt before the resolved-callee join.

The real-worker parity suite
witnesses worker boot, dispatch, and returned ParsedFiles, then compares complete
provider facts with direct extraction, including callerScope and C++ side-channel
data. Optional grammar availability is explicit in the test gate.

The persistence suite
loads both stores afresh from disk. Its parse-cache chunks contain no embedded
ParsedFiles, so warm equality requires durable-store reuse. It checks identical
CALLS/IMPORTS identities with zero extraction and zero worker dispatch on an
unchanged warm run. Editing a local import target or shadowing require removes
the stale target, and a second warm run preserves the edited graph.

Both suites are in the cross-platform manifest;
the shard regression
ensures each is selected exactly once across the four Windows/macOS shards.
CI owns native execution on those systems;
Ubuntu's full suite discovers the tests normally. These are focused evidence
boundaries, not a statement that the final full suite or every platform has passed.

Relationship to main and issue #3499

The integrated main baseline 50aa4be3 already includes #3502's nested Python
declaration ownership fix for the original #3499 case and #3504's aliased package
re-export fix. Their Python integration coverage
and module namespace coverage
remain relevant. This branch extends that base with the lexical claims, provider
semantics, and persistence evidence described above. Issue coverage is Related
#3499
; this document does not assert that all dynamic Python behavior is solved
or make an issue-status claim.


Languages Objective C Provider (`docs/languages/objective-c-provider.md`)

Objective-C Language Provider

Status: implemented

The deterministic provider is covered by focused unit and integration tests. The parser-loader ABI
smoke runs in the published multi-OS test matrix, and the native prebuild workflow owns
Objective-C together with all six vendored grammar targets. This status describes the implemented
MVP; it does not promise full Objective-C runtime dispatch.

Goal

Add deterministic, symbol-level Objective-C analysis to GitNexus. The first release must support high-confidence code navigation and direct static dependency analysis for .m, .mm, and Objective-C .h files. It must not imply that Objective-C runtime dispatch is fully resolved.

The provider belongs in the existing language-provider and scope-resolution extension points. Shared ingestion code must remain language-agnostic.

Compatibility contract

  • Existing language detection and parsing must remain unchanged.
  • A .h file must be classified from its content or surrounding context; it cannot be unconditionally claimed by Objective-C because C and C++ also use that extension.
  • If Objective-C grammar loading fails, the error must clearly name the missing provider/grammar and cannot corrupt a previously valid index.
  • Provider and grammar versions must be stored in index metadata. A version change that can alter node identity or edges requires a full rebuild.
  • No LLM participates in parsing, name resolution, or edge creation. Analysis is Tree-sitter plus deterministic static resolution.

MVP model

The provider must extract and connect:

  • Classes, superclasses, protocols, categories, class extensions, properties, ivars, C functions, imports, declarations, and implementations.
  • Instance and class methods, preserving their complete multi-part selector.
  • Inheritance, protocol conformance, import, declaration/implementation, host-class/category, and statically resolved call relationships.

Stable identity must include enough ownership to distinguish same-named methods. Recommended forms are:

text
objc:class:<ClassName>
objc:protocol:<ProtocolName>
objc:category:<HostClass>:<CategoryName>
objc:method:<Owner>:-:<selector>
objc:method:<Owner>:+:<selector>
objc:function:<qualified-or-file-scoped-name>

For example, -loadData:completion: and +loadData:completion: are different symbols. A category method remains linked to both its category and host class; querying the host class must expose distributed implementations.

Resolution policy

Resolution must be conservative. A missing or dynamic target is evidence of uncertainty, not proof that no target exists.

Receiver case Required result
Explicit class name, self, or super Resolve when the owner is statically known.
Local, parameter, property, or ivar with known static type Resolve to matching owner and selector.
Protocol-typed receiver Link the protocol method and identify possible implementations as candidates.
Multiple host-class/category implementations of one selector Record all static candidates as evidence; do not emit a certain call edge because runtime image-load order is unknown.
id, Class, macros, reflection, performSelector:, NSInvocation, runtime injection, or unknown type Store selector/location with resolution=unresolved; do not emit a certain call edge.

The provider should first collect file-local declarations, imports, and types, then resolve across the repository. It must use structured Tree-sitter captures or AST traversal, not regular expressions over source text. Multi-part selectors, block arguments, nullability annotations, generics, macros, and multiline declarations make a regex-only extractor unsafe.

Imports and incremental correctness

  • Resolve quoted project imports against the current directory, configured include roots, and indexed headers. Model framework imports as external-module evidence without downloading SDK source.
  • Merge @interface, @implementation, categories, and extensions across files.
  • A changed header, protocol, class declaration, or category invalidates importing and affected implementation/call-resolution state. Incremental output after such a change must match a full rebuild.
  • Index metadata must record provider version, grammar version, include/exclude configuration, and parsing options used for resolution.

Implementation sequence

  1. Add and package a pinned Objective-C Tree-sitter grammar; verify macOS arm64 and the production Linux runner can load it.
  2. Add language detection for .m, .mm, and content-classified .h files.
  3. Implement AST extraction and stable IDs for declarations and definitions.
  4. Implement repository-level merge, imports, inheritance, protocol, and category relationships.
  5. Add conservative message-send resolution and explicit unresolved evidence.
  6. Integrate invalidation, metadata comparison, MCP/CLI output, and fixtures.

Fixtures and acceptance

Create a minimal Objective-C fixture containing a class, protocol, category, extension, superclass, properties, ivars, C function, imports, multi-part selector, block parameter, self, super, protocol receiver, and id receiver. Use symodulebridge as a real integration fixture after the minimal suite is stable.

The acceptance bar is:

  • query "SYModuleCaller" yields class/method semantic nodes, not only file nodes.
  • context "SYModuleCaller" --file yields declaration, implementation, imports, and known references.
  • Known statically typed message sends create call edges; dynamic sends are marked unresolved.
  • Same selector on multiple classes, a category override, and + versus - methods remain distinct.
  • A .m, .h, protocol, or category edit produces results equivalent to a clean rebuild.
  • Generated documentation, dependency directories, and build output are excluded through explicit indexing configuration.

Non-goals

The MVP does not promise exact runtime type inference for id or instancetype, reflection, swizzling, arbitrary category replacement, dynamic selector construction, or complete impact analysis across every runtime dispatch path. Tool results must surface confidence and unresolved evidence rather than presenting guesses as certain graph facts.

Current implementation coverage

Implemented capabilities:

  • Vendored tree-sitter-objc grammar, registered through the existing Tree-sitter loader.
  • .m and .mm language mapping plus content-based .h classification so plain C/C++ headers are not unconditionally claimed.
  • LanguageProvider extraction for classes, protocols, categories, extensions, methods, properties, ivars, C functions, imports, unresolved message evidence, stable Objective-C qualified names, and provider/grammar metadata.
  • Length-preserving preprocessing of bare, file-scope all-caps macro markers before Tree-sitter parsing. This recovers declarations after wrappers such as RCT_EXTERN_C_BEGIN / RCT_EXTERN_C_END without expanding macros or adding framework-specific rules.
  • ScopeResolver edges for imports, inheritance, protocol conformance, category host membership, implementation evidence, and conservative static message sends.
  • Persisted query/context support for Objective-C class and method nodes, including implementation evidence via DECLARES.
  • Regression tests for grammar loading, .h classification, stable identities, conservative calls, metadata feature mismatch, persisted query/context behavior, and incremental-vs-force parity for Objective-C fixture edits.

Known limits of this MVP:

  • The first version does not perform full Objective-C runtime dispatch, swizzling, dynamic selector construction, macro expansion, or id flow inference. Bare file-scope marker macros are elided only to preserve parser recovery; their expansion semantics are not interpreted.
  • Protocol receiver handling records the protocol method and candidate implementation evidence, but candidate implementations are not emitted as certain call edges.
  • When a host class and one or more named categories define the same selector, the provider records candidate evidence rather than choosing a runtime winner or emitting multiple certain call edges.
  • Objective-C++ .mm files are parsed with the Objective-C grammar path for this MVP; deep C++ semantic extraction inside Objective-C++ bodies remains outside this provider.

Plans 2026 08 04 Gitnexus Plan Python Dotted Namespace Receiver (`docs/plans/2026-08-04-gitnexus-plan-python-dotted-namespace-receiver.md`)

GitNexus Engineering Plan

Task: Emit the missing CALLS edge for Python's unaliased multi-segment namespace import (import pkg.db + pkg.db.session_scope()), issue #2826.
Evidence verified at commit b2cd1c2ad637657125248c0dd2046de71ceea965; GitNexus index 13 commits behind HEAD, refresh skipped: every cited path is byte-identical between the index commit (1ef6447e) and the pinned commit โ€” verified by blob-id comparison, so no graph claim here rests on drifted content. PDG layer absent from this index (MATCH ()-[r:CodeRelation {type:'CDG'}]->() RETURN count(r) โ†’ 0); --pdg upgrade skipped, source reads substitute at higher evidence strength.
Evidence provenance schema 2; global dirty digest 0912a3ee3219cb75c82aefbf9f010e8dbe313150d6553768fd55d22af87a135c; cited-path manifest 13 sorted entries; exact generated plan path excluded.

1. Objective

import pkg.db followed by pkg.db.session_scope() must emit a CALLS edge from the caller to session_scope, matching the three sibling import spellings that already resolve (from pkg.db import session_scope, import pkg.db as pdb, from pkg import db). Two same-package imports in one file (import pkg.a + import pkg.b) must not cross-resolve, and no shared file under gitnexus/src/core/ingestion/ may name a language (AGENTS.md ยง42).

2. Current Behaviour

The failure is a key/lookup mismatch inside one map, not a missing resolution path.

For import pkg.db, splitImportStmt emits one match with @import.source = the whole dotted_name text "pkg.db" [verified] (gitnexus/src/core/ingestion/languages/python/import-decomposer.ts:46-54). interpretPythonImport's 'plain' arm then splits it [verified] (gitnexus/src/core/ingestion/languages/python/interpret.ts:33-42):

ts
case 'plain': {
      // `import numpy`
      if (sourceCap === undefined) return null;
      return {
        kind: 'namespace',
        localName: sourceCap.text.split('.')[0]!, // `import a.b.c` exposes `a`
        importedName: sourceCap.text,
        targetRaw: sourceCap.text,
      };
    }

finalizeImportEdges carries both halves onto the edge: localName verbatim, and targetExportedName = parsed.importedName for kind === 'namespace' [verified] (gitnexus-shared/src/scope-resolution/finalize-algorithm.ts:398-406, 434-447). So the finalized ImportEdge is { localName: 'pkg', targetExportedName: 'pkg.db', targetFile: 'pkg/db.py', kind: 'namespace' }.

collectNamespaceTargets keys only on localName [verified] (gitnexus/src/core/ingestion/scope-resolution/scope/namespace-targets.ts:44-57), producing {'pkg' โ†’ ['pkg/db.py']}.

At the call site, Python's query binds the attribute's object field with a wildcard โ€” object: (_) @reference.receiver [verified] (gitnexus/src/core/ingestion/languages/python/query.ts:267-270) โ€” so for pkg.db.session_scope() the receiver node is the inner attribute, and extractExplicitReceiver takes its raw text [verified] (gitnexus/src/core/ingestion/scope-extractor.ts:1235-1239): receiverName === 'pkg.db'.

emitReceiverBoundCalls then walks its cases [verified] (gitnexus/src/core/ingestion/scope-resolution/passes/receiver-bound-calls.ts:404-421, 546-655, 831-848):

  • Case 0 (compound receiver) fires because receiverName.includes('.') (line 563-567). It asks resolveCompoundReceiverClass for a class; pkg.db names a module, so it returns undefined, sets compoundReceiverUnresolved = true, and โ€” critically โ€” does not handledSites.add, so control falls through (lines 577, 622-655).
  • Case 1 (namespace receiver) runs namespaceTargets.get('pkg.db') (line 832). The map holds 'pkg'. Miss.
  • Case 1.5 needs provider.resolveQualifiedReceiverMember, implemented only by the C++ provider [verified] (gitnexus/src/core/ingestion/languages/cpp/scope-resolver.ts:399-406; context on that symbol shows one outgoing call to resolveCppQualifiedNamespaceMember and no other implementer). Python leaves it undefined, so the case is skipped.

No later case types a module receiver, so the site drops. Reproduced on both origin/main and PR #2810's head; PR #2810 changes Python receiver typing (languages/python/receiver-binding.ts) and does not touch this path [verified] by running the repro against both trees.

The three sibling spellings resolve because each binds a single-segment local name: pdb (alias arm), session_scope (named binding, not a receiver at all), and db (reclassified to kind: 'namespace' by #2770's isNamespaceImport hook, keying the map on 'db').

3. Relevant Architecture

collectNamespaceTargets is the shared, language-neutral bridge between finalized import edges and receiver resolution. Its contract note already states that ImportEdge.kind === 'namespace' is authoritative and that providers may reclassify into it โ€” that reclassification hook (isNamespaceImport) is #2770's extension point [verified] (gitnexus-shared/src/scope-resolution/finalize-algorithm.ts:99-107).

Its output feeds three consumers, all per-file (fileCompoundOpts, receiver-bound-calls.ts:405-406):

  1. emitReceiverBoundCalls Case 1 โ€” namespace-receiver member calls (receiver-bound-calls.ts:832);
  2. resolveConstructionExpressionClass โ€” namespace-qualified construction pkg.db.Model() (compound-receiver.ts:245-260);
  3. resolveCompoundReceiverClass's namespace-qualified-constructor disambiguation options.namespaceTargets?.has(objExpr) (compound-receiver.ts:759-766).

AGENTS.md line 42 is the binding constraint: "Shared code in gitnexus/src/core/ingestion/ must not name languages โ€” plug language behavior in via LanguageProvider / ScopeResolver hooks." [verified]

4. GitNexus Findings

  • context({name: 'collectNamespaceTargets', repo: 'GitNexus'}) โ€” epistemic: "exact"; incoming calls are exactly two: emitReceiverBoundCalls (.../passes/receiver-bound-calls.ts) and a test-local build in test/unit/scope-resolution/python/python-module-namespace-construction.test.ts. [graph] These are the d=1 dependents; the two compound-receiver.ts consumers reach the map by parameter rather than by call, so they do not appear here and were found by source grep [verified].
  • context({name: 'resolveQualifiedReceiverMember', repo: 'GitNexus'}) โ€” resolves to a single definition at languages/cpp/scope-resolver.ts:399, outgoing.calls: [resolveCppQualifiedNamespaceMember], no incoming. [graph] Confirms the Case-1.5 hook is C++-only, matching the issue reporter's read of the published bundle.
  • cypher({statement: "MATCH ()-[r:CodeRelation {type: 'CDG'}]->() RETURN count(r)"}) โ€” | cdg_rows | 0 |. [graph] The index carries no PDG layer; ยง5 is therefore empty by fact, not by omission.
  • Related tests located by directory listing [verified]: test/fixtures/lang-resolution/ already holds python-module-import, python-bare-import, python-plain-import-alias, python-multi-segment-ancestor-import, python-function-local-namespace-import, python-class-body-namespace-import, and #2770's python-from-module-alias. test/integration/resolvers/python.test.ts is the convention-matching home for the new assertions (#2770 added its coverage there, +38 lines).

5. Statement-Level PDG Findings

Empty by fact: the current index has zero CDG rows, so no statement-level slice exists to build. A --pdg re-index was deliberately not run โ€” it is the largest fixed cost available to this session, the analyzer holds no writer lock against a live MCP server (#2658), and every constraint the slice would supply (which case gates the namespace lookup, whether Case 0's failure falls through) was read directly from source at higher evidence strength in ยง2.

6. Proposed Changes

6.1 `collectNamespaceTargets` โ€” also key on the dotted access path

  • File: gitnexus/src/core/ingestion/scope-resolution/scope/namespace-targets.ts
  • Symbol: collectNamespaceTargets (source-verified)
  • Responsibility: map every receiver spelling that names an imported module to that module's file(s).
  • Change: inside the existing edge loop, after recording edge.localName, also record edge.targetExportedName when it contains a dot and its first dot-separated segment equals edge.localName. Same array-dedupe as the existing key.
  • Why this is language-neutral (AGENTS.md ยง42): the condition names no language. It encodes one structural fact โ€” a namespace binding whose exported module name is a dotted path rooted at the local name is also reachable under that whole path. Verified against every other namespace-emitting provider at the pinned commit [verified]:
    • TypeScript import * as X from './y' โ†’ localName 'X', importedName './y'; first segment '' โ‰  'X' โ†’ no key (languages/typescript/interpret.ts:77-81, 118-122).
    • C# using System.Collections.Generic โ†’ localName 'Generic' (last segment), importedName 'System.Collections.Generic'; first segment 'System' โ‰  'Generic' โ†’ no key (languages/csharp/interpret.ts:33-37, 62-66).
    • Go / Rust / Ruby โ†’ localName === importedName, no dot โ†’ no key (languages/{go,rust,ruby}/interpret.ts).
    • Python import pkg.db โ†’ 'pkg' === 'pkg.db'.split('.')[0] โ†’ key 'pkg.db' added. This is the only provider the predicate admits today.
  • Constraint: additive only. The existing localName key must keep its current value and ordering so no currently-resolving site changes target.
  • Two-package safety: import pkg.a + import pkg.b in one file yields {'pkg' โ†’ ['pkg/a.py','pkg/b.py'], 'pkg.a' โ†’ ['pkg/a.py'], 'pkg.b' โ†’ ['pkg/b.py']}. Receiver pkg.a hits exactly one file; the ambiguous 'pkg' bucket is only reachable by a receiver literally spelled pkg, which is unchanged from today. [inferred] โ€” pinned by a test in ยง8.

6.2 `isNamespaceNameShadowed` โ€” test the root segment, not the dotted path

  • File: gitnexus/src/core/ingestion/scope-resolution/passes/compound-receiver.ts
  • Symbol: isNamespaceNameShadowed (source-verified, lines 152-183) and its one call site at line 250.
  • Defect this fix activates: the guard walks the scope chain looking for a binding, type binding, lexical name, or owned def named exactly namespaceName. With 6.1 in place, namespaceName can be 'pkg.db', but Python binds only pkg โ€” so a local pkg = something that genuinely shadows the import would fail to suppress the namespace interpretation, and the "verified namespace is authoritative" branch (line 249-259) would return a wrong class instead of declining.
  • Change: shadow-test the first dot-separated segment of namespaceName (identical behaviour for the single-segment names it sees today, since root === whole name).
  • Not scope creep: 6.1 is what first routes a dotted name into this guard; shipping 6.1 without it introduces the false positive.

6.3 No change required in `receiver-bound-calls.ts`

Case 1's lookup already uses the full dotted receiverName and Case 0's failure already falls through to it (receiver-bound-calls.ts:577, 622-655, 832) [verified]. Recorded here so the executor does not "fix" a path that is already correct.

7. Implementation Sequence

  1. Add the failing fixture and assertions first. Create gitnexus/test/fixtures/lang-resolution/python-dotted-namespace-import/ (files in ยง8) and a describe block in gitnexus/test/integration/resolvers/python.test.ts following the file's existing writeFixtureRepo + mkdtempSync convention. Confirm the dotted row fails and all three control rows pass. Delete the scratch gitnexus/test/integration/resolvers/repro-2826-python-dotted-import.test.ts in this step โ€” its content is superseded by the fixture-backed tests.
  2. Implement 6.1 in namespace-targets.ts, and update its header contract note to state that a namespace edge may be keyed both by its local name and by a dotted access path rooted at that name. Re-run the step-1 tests: the dotted row must flip to passing with the controls still green.
  3. Implement 6.2 in compound-receiver.ts with the shadowing test from ยง8 (a local pkg = Decoy() must suppress, not misresolve).
  4. Run the regression surface: full resolver + scope-resolution integration suites, both packages' tsc --noEmit.
  5. Regenerate recorded baselines once, last. Run each --check gate; regenerate only the baselines that actually moved (bench/receiver-resolution/baseline.json is the expected one โ€” this change adds resolved edges). Per plan-template ยง7, this is deliberately the final step so intermediate commits do not churn and re-drift the artifacts.

8. Test Strategy

New fixture gitnexus/test/fixtures/lang-resolution/python-dotted-namespace-import/:

file contents
pkg/__init__.py empty
pkg/db.py def session_scope(): ...
pkg/cache.py def session_scope(): ... โ€” the decoy that makes cross-resolution detectable
caller_dotted.py import pkg.db + def uses_dotted(): return pkg.db.session_scope()
caller_from.py, caller_alias.py, caller_frommod.py the three sibling controls from the issue
caller_two_pkgs.py import pkg.db and import pkg.cache, one function calling each
caller_deep.py import pkg.sub.deep + pkg.sub.deep.f() (3-segment)
caller_shadowed.py module-level import pkg.db, then a function with a local pkg = Decoy() before pkg.db.session_scope()

Scenarios (input โ†’ action โ†’ expected):

  1. caller_dotted.py โ†’ run pipeline โ†’ CALLS edge uses_dotted โ†’ pkg/db.py:session_scope, reason: 'import-resolved'. This is the issue's acceptance row.
  2. The three sibling callers โ†’ same run โ†’ all three still resolve to pkg/db.py:session_scope. Regression control: a run where the controls also broke would prove nothing about row 1.
  3. caller_two_pkgs.py โ†’ pkg.db.session_scope() resolves only to pkg/db.py and pkg.cache.session_scope() only to pkg/cache.py; assert the absence of the crossed pair explicitly, not just the presence of the right one.
  4. caller_deep.py โ†’ 3-segment receiver resolves โ€” proves the predicate is not hard-coded to two segments.
  5. caller_shadowed.py โ†’ no edge from the shadowed function to pkg/db.py (6.2's guard). Fails loudly if 6.2 regresses.
  6. Cross-language non-regression: the existing TypeScript / C# / Go namespace-import resolver tests must stay green unchanged โ€” that is the executable proof the new key is not minted for them.

Tests to update: gitnexus/test/integration/resolvers/python.test.ts (add the describe block). gitnexus/test/unit/scope-resolution/python/python-module-namespace-construction.test.ts is a direct collectNamespaceTargets caller โ€” re-run it; extend it only if its expectations enumerate map keys exhaustively.

Verification commands (each verified to exist in gitnexus/package.json / .github/workflows/ci-tests.yml at the pinned commit):

bash
# from gitnexus/ โ€” pretest:integration runs scripts/build.js, so the parse worker exists
GITNEXUS_WORKER_READY_TIMEOUT_MS=60000 npm run test:integration -- test/integration/resolvers/python.test.ts
GITNEXUS_WORKER_READY_TIMEOUT_MS=60000 npm run test:integration -- test/integration/resolvers
npm run test:unit -- test/unit/scope-resolution
npx tsc --noEmit                     # and the same in ../gitnexus-shared
node --import tsx bench/receiver-resolution/measure.mjs --check
node --import tsx bench/python-scope/measure.mjs --check
node --import tsx bench/python-scope/import-target-fingerprint.mjs --check
node --import tsx bench/scope-capture/measure.mjs --check

GITNEXUS_WORKER_READY_TIMEOUT_MS=60000 is required on this host: the default 5000 ms worker-ready deadline fails as a crash-loop here (observed while reproducing the issue), which is environmental, not a code fault.

9. Risk and Impact Analysis

Accounting for every direct (d=1) dependent of the changed map:

d=1 dependent risk mitigation
emitReceiverBoundCalls Case 1 (receiver-bound-calls.ts:832) New keys make previously-dropped sites resolve. A wrong target would be a new false edge. The predicate admits only Python's import a.b shape; each new key maps to exactly one file per import statement. ยง8 scenario 3 pins non-crossing.
resolveConstructionExpressionClass (compound-receiver.ts:245-260) pkg.db.Model() now takes the "verified namespace is authoritative" branch, which deliberately does not fall through on a miss or ambiguity โ€” so a wrong key would convert a working heuristic resolution into a silent decline. The branch requires namespaceFiles.length > 0, i.e. the import genuinely resolved. Ambiguity still returns undefined (namespaceMatches.length === 1 guard). Shadowing is fixed by 6.2.
resolveCompoundReceiverClass namespace-constructor disambiguation (compound-receiver.ts:759-766) namespaceTargets.has(objExpr) now true for dotted namespaces, routing pkg.db.Model(x).run() into the construction interpretation. Correct by intent โ€” that branch exists precisely to make a namespace-qualified bare constructor safe. Behaviour change, so ยง8 should include a construction row if the fixture's cost is low.
test/unit/scope-resolution/python/python-module-namespace-construction.test.ts:build May assert exact map contents. Re-run in step 4; extend rather than weaken if it enumerates keys.
C++ provider Case 1 is skipped entirely for C++ (provider.resolveQualifiedReceiverMember !== undefined), but the two compound-receiver.ts consumers are not provider-gated. C++ #include does not produce a kind: 'namespace' edge with a dotted targetExportedName rooted at its local name; the predicate declines. Covered by the existing C++ suites plus bench/cpp-qualified-ns/measure.mjs --check.

Recorded-artifact risk: bench/receiver-resolution/measure.mjs --check gates both a shape matrix and a drop-count arm; new resolved edges are expected to move the count arm and the gate fails on drift. Regenerating in step 5 only (per ยง7) keeps intermediate commits clean. bench/python-scope/* and bench/scope-capture/* fingerprint captures and import-target resolution โ€” neither is touched by this change, so a movement there is a signal to stop and investigate, not to regenerate.

Performance: one extra Map.set per multi-segment namespace import per file; the loop is already O(module import edges). No new traversal.

No schema/version impact: this changes what the resolver produces, not how it is stored. Existing indexes need a re-analyze to show the new edges โ€” matching the note PR #2810 carried for the same reason.

10. Files Expected to Change

File Symbols Reason
gitnexus/src/core/ingestion/scope-resolution/scope/namespace-targets.ts collectNamespaceTargets Add the dotted-access-path key (ยง6.1) and update the contract note
gitnexus/src/core/ingestion/scope-resolution/passes/compound-receiver.ts isNamespaceNameShadowed Shadow-test the root segment (ยง6.2)
gitnexus/test/integration/resolvers/python.test.ts new describe block Issue acceptance row + controls + regression rows
gitnexus/test/fixtures/lang-resolution/python-dotted-namespace-import/** โ€” New fixture (ยง8)
gitnexus/test/integration/resolvers/repro-2826-python-dotted-import.test.ts โ€” Delete; superseded by the fixture-backed tests
gitnexus/bench/receiver-resolution/baseline.json โ€” Regenerate once, final step, only if --check moves

11. Reusable Implementation Context

text
/* Detailed source-code truncated for AI context efficiency. */

12. Assumptions and Open Questions

Assumptions (each re-checkable cheaply by the executor):

  1. Every non-Python namespace-emitting provider fails the dotted && first segment === localName predicate. Verified at b2cd1c2ad for TypeScript, C#, Go, Rust and Ruby by reading each interpret.ts; JavaScript, Java and PHP emit no kind: 'namespace' import there. Re-check: grep kind: 'namespace' across gitnexus/src/core/ingestion/languages/*/interpret.ts.
  2. python.test.ts runs unconditionally โ€” no REGISTRY_PRIMARY_PYTHON gate remains at the pinned commit (zero grep hits). An older parity-leg convention no longer applies.
  3. The index's 13-commit lag is harmless here because every cited path is byte-identical at the index commit and the pinned commit.

Open questions / explicitly deferred:

  • The bogus first-segment key. For import pkg.db, the map still holds 'pkg' โ†’ ['pkg/db.py'], so pkg.helper() would resolve into pkg/db.py if that file happens to define helper โ€” a pre-existing false positive this plan does not fix. Removing it is a separate behaviour change with its own regression surface (python-multi-segment-ancestor-import, python-bare-import). Worth pinning the current behaviour in a test so it is visible rather than silent.
  • import a.b.c also binds a.b. Python makes intermediate packages reachable; the proposed predicate keys only the exact imported path, so a.b.f() under import a.b.c alone stays unresolved. Narrower follow-up.
  • C# has the mirror-image gap. using System.Collections.Generic + System.Collections.Generic.List fails the predicate because C# sets localName to the last segment. Deliberately out of scope; deserves its own issue.
  • Construction coverage. ยง8 does not currently include a pkg.db.Model() row. Add one if the fixture cost is trivial โ€” that path (compound-receiver.ts:245-260) changes behaviour and is otherwise untested by this plan.

13. Definition of Done

  1. CALLS edge uses_dotted โ†’ pkg/db.py:session_scope (reason: 'import-resolved') is emitted, asserted by a fixture-backed test in python.test.ts.
  2. All three sibling control rows still resolve in the same run.
  3. import pkg.a + import pkg.b in one file resolve only to their own modules; the crossed pair is asserted absent.
  4. A 3-segment receiver resolves; a package root shadowed by a local binding does not.
  5. The scratch repro-2826-python-dotted-import.test.ts is deleted.
  6. No file under gitnexus/src/core/ingestion/ names a language.
  7. npm run test:integration -- test/integration/resolvers and npm run test:unit -- test/unit/scope-resolution pass; tsc --noEmit clean in both packages.
  8. Every bench --check in ยง8 passes, with bench/receiver-resolution/baseline.json regenerated exactly once in the final commit if and only if it moved โ€” and any movement in python-scope/scope-capture investigated rather than regenerated.

Plans 2026 08 28 Gitnexus Plan Impact File Risk (`docs/plans/2026-08-28-gitnexus-plan-impact-file-risk.md`)

GitNexus Engineering Plan

Task: Fix #3075 โ€” File impact risk is not comparable to Function/Method risk.
Evidence verified at commit 6bff33d14cbfe1e7b4f04bca51507e9f64ef579c (feat/kotlin-const-resolver); GitNexus index 129 commits behind, refresh skipped: full-repo --index-only --pdg rebuild is impractical this session. Scorer and schema claims are [verified] from source; live inversion numbers are [graph] on the stale index.

1. Objective

Make File vs symbol impact.risk honest for consumers: either they can tell the scales differ, or they can compare on a shared two-axis score. Do not DEFINES-bridge processes/modules onto File targets (issue reporter sampled 8/10 one-importer files jumping to HIGH/CRITICAL). Do not retune Function HIGH/CRITICAL thresholds (agent warn-before-edit).

Acceptance:

  • A File with a wider blast radius than a Function in the same file no longer looks โ€œsaferโ€ when a consumer only reads risk, or the result states that risk is not comparable across kinds and offers riskSharedAxes for comparison.
  • File targets still cannot trip HIGH/CRITICAL via processes_affected / modules_affected unless those axes become real in the index (they are not today).
  • Existing Function/Method labels under the current four-axis ladder stay the same for the same inputs.
  • MCP riskNote remains UNKNOWN-only (tools.ts contract).

2. Current Behaviour

Callgraph impact ends in LocalBackend._runImpactBFS (gitnexus/src/mcp/local/local-backend.ts). After BFS it enriches impacted ids with STEP_IN_PROCESS and MEMBER_OF, then scores:

7720:7738:gitnexus/src/mcp/local/local-backend.ts
} else if (
      directCount >= 30 ||
      processCount >= 5 ||
      moduleCount >= 5 ||
      impacted.length >= 200
    ) {
      risk = 'CRITICAL';
    } else if (
      directCount >= 15 ||
      processCount >= 3 ||
      moduleCount >= 3 ||
      impacted.length >= 100
    ) {
      risk = 'HIGH';
    } else if (directCount >= 5 || impacted.length >= 30) {
      risk = 'MEDIUM';
    } else {
      risk = 'LOW';
    }

Empty upstream โ†’ UNKNOWN + riskNote. Downstream empty stays LOW. skipEnrichment (ambiguous probes) already scores on direct+total only. PDG mode forces risk: UNKNOWN (composeUnifiedPdgImpactResult) โ€” out of scope.

File BFS walk is mostly Fileโ†IMPORTS File. Enrichment queries those File ids. Processes are CALLS traces (process-processor.ts); communities admit only Function/Class/Method/Interface (isCommunitySymbol in community-processor.ts:412-416). File is not in that set. enrichCandidateLabels UNION also omits File, so File target.type is often ""; detect File via id prefix File:.

Web Graph RAG (gitnexus-web/src/core/llm/tools.ts ~1331โ€“1346) duplicates the same ladder.

3. Relevant Architecture

Layer Role
Index File never sources STEP_IN_PROCESS / MEMBER_OF by construction
MCP _runImpactBFS Blast radius + four-axis risk
Ambiguous probes skipEnrichment โ†’ 2-axis risk already
mergeRisk Group overlay; monotone in crossings; does not know target kind
CLI formatImpactResult Prints counts; does not print risk on the resolved callgraph path; JSON impactCommand still ships risk
ai-context.ts / tools.ts Agent contract: warn on HIGH/CRITICAL; riskNote UNKNOWN-only
Web LLM impact Same formula, prose RISK: line

Modules: Local (MCP), Cli (format/docs), Group (mergeRisk), gitnexus-web LLM tools. Shared package gitnexus-shared is already a dependency of both CLI and web.

4. GitNexus Findings

  • Primary: _runImpactBFS โ€” d=1 [graph] impact(target:_runImpactBFS, maxDepth:1, includeTests:true): _impactImpl, impactByUid. Production chain [verified]: impact โ†’ _impactImpl โ†’ _runImpactBFS; impactByUid skips per-symbol process lists but not aggregation (skipPerSymbolEnrichment only).
  • LocalBackend.impact d=1 [graph] context: callTool.
  • Duplicate scorer [verified] grep: gitnexus-web/src/core/llm/tools.ts.
  • mergeRisk [verified] callers in src/: only runGroupImpact (cross-impact.ts:907). Graph d=1 listed a test File (impact-pdg-shape.test.ts) and missed runGroupImpact โ€” trust source.
  • Schema [verified]: isCommunitySymbol excludes File; schema.ts documents MEMBER_OF as Function/Class/Method/Interface only.
  • Live inversion [graph] stale index, impact summaryOnly on GitNexus:
target kind impacted direct processes modules risk
lbug-config.ts File 54 12 0 0 MEDIUM
openLbugConnection Function 16 9 3 2 HIGH
local-backend.ts File 12 10 0 0 MEDIUM
refreshRepos Method 50 5 4 7 CRITICAL
  • Clusters/processes resources [graph]: Local/Cli/Group sit in the impact path; process traces are function-stepped, not File-stepped.
  • Related tests [verified]: test/unit/impact-pagination.test.ts (CRITICAL from direct=400); test/integration/impact-zero-caller-risk.test.ts (withTestLbugDB seed โ€” pattern to extend); test/unit/eval-formatters.test.ts (formatImpactResult); group mergeRisk tests.

5. Statement-Level PDG Findings

PDG unavailable (pdg_query on _runImpactBFS: โ€œno PDG layerโ€). Recommend node .gitnexus/run.cjs analyze --index-only --pdg before any future statement-slice work. Control flow of the scorer is a straight if/else after enrichment; no hidden guards. skipEnrichment is the only branch that structurally zeros process/module counts besides File ids.

6. Proposed Changes

6.1 Extract `scoreImpactRisk` โ€” `gitnexus-shared/src/impact-risk.ts` (new)

  • Responsibility: Pure function: { direction, directCount, processCount, moduleCount, impactedCount, unusedAxes } โ†’ { risk, riskSharedAxes, riskScale }.
  • Behaviour: Existing UNKNOWN/CRITICAL/HIGH/MEDIUM/LOW thresholds unchanged when unusedAxes is empty. riskSharedAxes always scores as if processCount=0 and moduleCount=0 (UNKNOWN rule still applies). riskScale.comparableAcrossKinds is false iff unusedAxes is non-empty. riskScale.unusedAxes lists { axis, reason }.
  • Constraints: Zero deps. Export from gitnexus-shared/src/index.ts. Do not put MCP types here.
  • File detection: caller passes unused axes; helper does not parse UIDs.

6.2 Wire MCP โ€” `_runImpactBFS` in `local-backend.ts`

  • After computing processCount/moduleCount, set unusedAxes:
    • target id starts with File: or symType === 'File' โ†’ processes + modules, reason file-nodes-have-no-process-or-community-membership;
    • skipEnrichment โ†’ same axes, reason enrichment-skipped (ambiguous probes).
  • Replace inline ladder with scoreImpactRisk.
  • Spread riskScale and riskSharedAxes on the result next to risk. Do not set riskNote for File.
  • Ambiguous candidate summaries: forward the new fields (probes already skip enrichment).
  • target.type for File: if still "", prefer 'File' when id starts with File: (display-only; helps CLI).

6.3 Web duplicate โ€” `gitnexus-web/src/core/llm/tools.ts`

  • Import scoreImpactRisk from gitnexus-shared. Print RISK: from risk; if !comparableAcrossKinds, one extra line: not comparable to Function risk; shared-axes label is riskSharedAxes.

6.4 Agent/MCP contract copy

  • gitnexus/src/mcp/tools.ts impact description: document riskScale / riskSharedAxes; keep riskNote UNKNOWN-only; say File risk is not comparable to symbol risk.
  • gitnexus/src/cli/ai-context.ts: HIGH/CRITICAL warning still applies; add: do not rank a File MEDIUM below a contained Function HIGH without riskSharedAxes.
  • formatImpactResult: on resolved callgraph results with risk, print Risk: {risk} and, when incomparable, Shared-axes risk: {riskSharedAxes} (File/process axes unused).

6.5 Explicitly not changing

  • DEFINES-bridge, community/process indexers, mergeRisk formula, PDG UNKNOWN, detectChanges risk_level, Function thresholds.

7. Implementation Sequence

  1. Add gitnexus-shared helper + unit table (issue-shaped inputs + UNKNOWN + skipEnrichment). Shared package tests if present; otherwise gitnexus/test/unit/impact-risk.test.ts importing the helper.
  2. Switch _runImpactBFS + candidate probe payload. Tree still coherent: old risk values identical for Function fixtures.
  3. Integration seed in impact-zero-caller-risk.test.ts or new impact-file-risk-scale.test.ts: File with โ‰ฅ5 File IMPORTS (MEDIUM on direct) vs Function with 3 process-member callers (HIGH); assert File riskScale.comparableAcrossKinds === false, Function true, File riskSharedAxes === risk, Function riskSharedAxes is LOW/MEDIUM while risk is HIGH.
  4. CLI formatter + eval-formatters.test.ts.
  5. tools.ts + ai-context.ts wording.
  6. Web import + a unit assertion on the printed RISK block if a test already covers that tool.
  7. npx tsc --noEmit in gitnexus/ and gitnexus-web/; cd gitnexus && npm run test:unit -- test/unit/impact-risk.test.ts test/unit/eval-formatters.test.ts; integration file from step 3.

8. Test Strategy

File Scenarios
gitnexus/test/unit/impact-risk.test.ts (new) Issue table: File(25,13,0,0)โ†’MEDIUM; Function(15,2,4,2)โ†’HIGH; shared-axes File MEDIUM vs Function LOW; empty upstream UNKNOWN; downstream empty LOW; skipEnrichment unused axes; CRITICAL via directโ‰ฅ30 still works with unused process axes
gitnexus/test/integration/impact-file-risk-scale.test.ts (new) withTestLbugDB seed: File:src/crypto.ts โ† 13 File IMPORTS, no File STEP_IN_PROCESS; getEncryptionKey with 2 CALLS from functions that have STEP_IN_PROCESS to 4 distinct Process nodes โ€” reproduce inversion; assert new fields
gitnexus/test/integration/impact-zero-caller-risk.test.ts Unchanged UNKNOWN/riskNote; candidates may grow riskScale โ€” assert still present only when UNKNOWN for riskNote
gitnexus/test/unit/impact-pagination.test.ts Hub CRITICAL unchanged
gitnexus/test/unit/eval-formatters.test.ts Resolved result prints Risk + shared-axes line for File-shaped riskScale
Web Only if an existing Graph RAG impact test snapshots RISK:

Commands (exist in gitnexus/package.json): npm run test:unit, npm test (full vitest), npx tsc --noEmit. Web: npm test, npx tsc -b --noEmit. Integration needs pretest:integration / npm run test:integration (runs scripts/build.js).

9. Risk and Impact Analysis

Direct dependents of _runImpactBFS [graph]: _impactImpl, impactByUid. _impactImpl is the only d=1 of impact besides the methodโ€™s own class. Any JSON consumer of impact (MCP, CLI output(result), group local leg) sees additive fields โ€” compatible if they ignore unknowns.

  • HIGH workflow: Function HIGH/CRITICAL unchanged. File still cannot reach HIGH via processes; a File with directโ‰ฅ15 or totalโ‰ฅ100 still can. Agents that compare File MEDIUM vs Function HIGH must start using riskSharedAxes or riskScale.
  • Ambiguous maxRisk: probes skip enrichment, so File vs Function candidates are already 2-axis there โ€” inversion is weaker on that path.
  • Group mergeRisk: still compares incomparable File local risk to crossing count. Do not retune this PR; if a group File target is common, follow-up.
  • Web: browser bundle picks up gitnexus-shared export โ€” confirm gitnexus-shared build/exports include the new file.
  • Performance: none (pure arithmetic after existing enrichment).
  • Ladybug empty labels: File detection must not rely on symType alone.

10. Files Expected to Change

File Symbols Reason
gitnexus-shared/src/impact-risk.ts scoreImpactRisk New shared scorer
gitnexus-shared/src/index.ts exports Public helper
gitnexus/src/mcp/local/local-backend.ts _runImpactBFS, ambiguous candidate map Wire scorer + File unused axes
gitnexus/src/mcp/tools.ts impact description Contract
gitnexus/src/cli/ai-context.ts generated Always Do Agent warning
gitnexus/src/cli/eval-server.ts formatImpactResult Print scale
gitnexus-web/src/core/llm/tools.ts web impact Same formula
gitnexus/test/unit/impact-risk.test.ts โ€” Table tests
gitnexus/test/integration/impact-file-risk-scale.test.ts โ€” Seeded inversion
gitnexus/test/unit/eval-formatters.test.ts formatImpactResult Formatter

11. Reusable Implementation Context

text
/* Detailed source-code truncated for AI context efficiency. */

12. Assumptions and Open Questions

Assumptions

  • Indexer will not start attaching Fileโ†’Process/Community in this change (isCommunitySymbol stays). [verified] source; [assumed] future indexers.
  • Ignoring unknown JSON keys is safe for MCP clients; any toEqual goldens in-repo must be updated. [assumed] โ€” grep during implement.
  • Stale-index inversion (lbug-config.ts vs openLbugConnection) is illustrative; the integration seed is the regression lock. [graph] vs [verified] seed.

Open questions

  • Group mergeRisk + File local risk: copy riskScale onto GroupImpactResult? Default no unless a test breaks.
  • Class/Interface STEP_IN_PROCESS sparsity: out of scope (#3075 is File).
  • Printing risk on CLI formatted output is new (JSON already has it). Keep the extra lines short.

Deferred

  • Recalibrated File-only HIGH thresholds.
  • Indexing File community membership.
  • DEFINES-bridge after a threshold RFC.
  • Related #2975 (docs vs scorer wording) except as touched by tools.ts.

13. Definition of Done

  • scoreImpactRisk is the only callgraph ladder in MCP and web.
  • File (and skipEnrichment) results include riskScale.comparableAcrossKinds === false and riskSharedAxes.
  • Function four-axis HIGH/CRITICAL cases in unit tests still pass with the same labels.
  • Integration seed proves wider File blast + lower risk than a contained Function, and riskSharedAxes orders them without pretending processes existed on the File.
  • riskNote still absent unless risk === 'UNKNOWN'.
  • tools.ts + ai-context.ts state that File risk is not comparable to symbol risk.
  • cd gitnexus && npx tsc --noEmit and the named unit/integration commands pass; web typecheck passes.

--- METRICS ---