Find a file
Gergő Magyar 997fc05b83
fix(resolution): resolve calls through a generic-typed field receiver in every language (#2833) (#2855)
* test(resolution): pin generic-typed field receivers across languages (#2833)

A field whose declared type carries a type argument (`repo: Repo<User>`)
emits zero CALLS edges — not a truncated chain, not an edge to the
interface declaration, nothing. This adds the cross-language matrix that
measures it, modelled on the #2807 inferred-field matrix: every language
runs the same two calls, one through a generic-typed field and one
through a non-generic control field, and each language is compared
against its OWN control row rather than an absolute edge count.

Measured state, pinned here as `known-gap` so the file is green on main
and flipping a row is a visible edit:

  affected    TypeScript, C#, C++, Python
  unaffected  Java, Kotlin, Go, Rust, Swift, Dart

The unaffected six erase type arguments at interpret time (Java's
`stripGeneric`, F41 #1928; Swift likewise). TypeScript, C# and Python
instead run a container ALLOW-LIST that returns the type ARGUMENT, so a
user-defined `Repo<User>` survives verbatim into a lookup that binds
nothing.

The `ts-local-vs-field` case is the bug in one file: `viaLocal` and
`viaParam` both resolve for the identical type, and only `viaField`
loses every edge — a bare name reaches Case 4 and its generic-aware
lookup, a dotted field receiver does not.

Negative controls pin what erasure must NOT do: an unbounded type
parameter denotes no declaration, and a C++ explicit specialization is a
different class from its primary template. The `Box2<T>` row pins a
PRE-EXISTING false edge (a workspace class named `T`) so it cannot later
be mistaken for fallout from this work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp

* refactor(resolution): move resolveClassBindingForName to the shared walkers (#2833)

Pure relocation, no behaviour change: the generic-aware class lookup
moves from `passes/receiver-bound-calls.ts` to `scope/walkers.ts`, beside
the bare `findClassBindingInScope` it wraps. Its two existing callers —
`classifyReceiverOrigin` and Case 4 — import it from the new home and are
otherwise untouched.

The move is required rather than cosmetic: `receiver-bound-calls.ts`
already imports from `compound-receiver.ts`, so having the compound
receiver call into the pass would close an import cycle. `walkers.ts` is
the shared floor both already depend on.

Verified behaviour-neutral: the #2833 matrix is 44/44 identical before
and after, across all fifteen fixtures.

detect_changes attributes `resolveInheritanceBaseInScope`,
`resolveQualifiedInheritanceBase` and `EMPTY_BINDINGS` to this commit;
those are line-shift artifacts of inserting a function above them, and
their bodies are byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp

* fix(resolution): type generic field receivers through the generic-aware lookup (#2833)

A field receiver is spelled `this.repo` — dotted — so it types through
the receiver-chain fold and the text cascade, both of which reach
`findClassBindingInScope`. That function has no notion of type arguments,
so a field declared `Repo<User>` resolved to nothing and the call site
emitted NO edge at all: not the interface declaration, not the
implementation fan-out, nothing. A local or parameter of the identical
type is a bare name, reaches Case 4 and its generic-aware
`resolveClassBindingForName`, and resolved fine. The bug was the
asymmetry, not the generics.

Three receiver-typing lookups now call the generic-aware helper instead:
`typeOfMemberOnClass`'s primary and module-hoist branches, and the
cascade's bare-identifier type-binding read. Every other one of the 38
`findClassBindingInScope` call sites is untouched — its own docstring
records that widening it globally suppresses the `?? otherResolver(...)`
fallbacks two dozen callers rely on, which would retarget inheritance
edges, and impact rates it CRITICAL with 12 direct dependents.

Order matters and is preserved: the helper tries the exact name, then an
arity- and token-exact match against `def.templateArguments`, and only
then falls back to the base name. Erasing first would collapse a C++
explicit specialization onto its primary template — `Vec<bool>` really is
a different class. A bare type parameter carries no type arguments, so it
never enters the generic branch and cannot be erased into a class that
happens to share its name.

Measured: TypeScript and C# generic-typed fields now emit exactly what
their non-generic control rows emit, primary plus interface-dispatch
fan-out. Java, Kotlin, Go, Rust, Swift and Dart are byte-identical. Both
type-parameter negative controls are unchanged. C++ and Python are still
open and stay pinned as known-gaps — they fail for different reasons and
get their own commits.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp

* fix(cpp,python): bind generic-typed member fields so their calls resolve (#2833)

Completes #2833 for the two languages the shared resolution change could
not reach. Each failed for its own reason, and both were found by
measurement rather than assumed.

C++ — a CAPTURE gap, not a resolution one. All three `field_declaration`
type-binding rules required `type: (type_identifier)`, so a member
declared `Repo<User> repo;` is a `template_type` and matched none of
them: the field got no type binding at all, and every call through it
lost its edge in both the bare and `this->` spellings. A LOCAL of the
identical type resolved the whole time, because the local declaration
rules gained their `template_type` variant long ago. Three mirrored
rules close it, one per declarator shape (plain, pointer, reference).
Written as separate patterns rather than one alternation: a node-type
alternation in a field position is a tree-sitter 0.21 hazard this repo
has been bitten by before.

Python — the bracket spelling never entered the generic branch. Its
`stripGeneric` is a container allow-list over `[...]` that returns the
type ARGUMENT (`list[User]` to `User`), so a user-defined `Repo[User]`
matched nothing and survived verbatim, and the shared lookup's generic
branch is gated on `<`. It now reduces a subscripted type neither
allow-list claims to its base name — the same rule Java and Swift
already apply to `<...>`. Deliberately the LAST resort: a container must
reach its own rule first, or `list[User]` would type the receiver as the
container and retarget every call in a for-loop chain. The as-written
spelling survives on `TypeRef.declaredSpelling`, which is what the fold's
index step reads.

Both are parse-time and land in the cached ParsedFile, so SCHEMA_BUMP
goes 45 -> 46 with its pin test. Verified free against origin/main; the
ledger in that file records three prior EXACT clashes, so re-check again
immediately before merge.

The matrix now covers the spellings real code writes, all measured: a
nullable generic, a bounded wildcard, a raw type, a nested generic and a
multi-argument one. None needed work beyond the shared lookup, which is
the evidence that base-name erasure is the right primitive. The C++
specialization control now asserts what it was written for:
`Vec<bool>.save` and `Vec.save` are DIFFERENT target ids, so the
arity/token match still wins over erasure.

scope-capture is byte-identical for cpp and c, so no rebaseline — the
bench corpus contains no generic-typed member field, which is worth its
own coverage issue.

Two pre-existing gaps were measured and are deliberately NOT fixed here,
because in both cases the language's own non-generic CONTROL row fails
identically: C++ `this->field.m()` emits nothing, and JavaScript/PHP
docblock-declared field types bind nothing at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp

* fix(python): do not reduce containers or typing special forms to a base name (#2833)

Review finding on this branch's own Python change, caught by probing the
interpreter directly rather than by reading it.

The base-name reduction was reached by FALLTHROUGH: "neither container rule
matched" was treated as "not a container". It is not, and two measured
shapes proved it:

  dict[str, list[User]]   ->  dict          (was: the annotation, intact)
  Dict[str, Repo[User]]   ->  Dict
  Callable[[int], User]   ->  Callable
  Literal["a"]            ->  Literal
  Union[A, B]             ->  Union
  tuple[int, ...]         ->  tuple

The dict rule's value group cannot span a nested `]`, so a nested value
declines and falls through — and the dict rule's own comment says that
shape is deliberately "left for a downstream strip pass". Collapsing it to
`dict` destroyed the value type instead. The typing SPECIAL FORMS are worse:
`Callable`, `Literal`, `Annotated` and `Union` are not classes, and reducing
them to a bare name binds any workspace class that happens to share it —
a fabricated edge, which is strictly worse than the missing edge #2833 set
out to fix, and those names are ordinary enough for a real codebase to
declare.

Reduction is now guarded by an explicit deny set covering the containers the
two allow-lists already own and the typing special forms. Everything named
there keeps its as-written text and resolves exactly as it did before #2833.

`arr[0]` also reduces to `arr` in isolation, but that is unreachable and is
now documented as such: every Python `@type-binding.type` capture is a
`(type)`, `(identifier)`, `(attribute)` or `(dotted_name)` node, so a
subscripted VALUE expression never reaches the interpreter.

Pinned by a new unit test that asserts all four groups — user generic
reduces, container reduces to its ELEMENT, declined container shape stays
intact, special form untouched. Reverting the deny set fails three of its
five cases.

Also corrects `resolveClassBindingForName`'s docstring, which this branch
had made false: it claimed only `classifyReceiverOrigin` passes the
decoration stripper, while the three receiver-typing lookups in
compound-receiver.ts now pass it too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp

* fix(resolution): rank base-name candidates lexically and refuse arg-pinned defs (#2833)

Review of #2855 found that this PR turned a MISSING C++ edge into a
CONFIDENTLY WRONG one — the direction this subsystem calls unrecoverable.

`resolveClassBindingForName` ended with an unguarded base-name fallback
that returned the first same-named class the scope chain reached. A C++
primary template carries `templateArguments === undefined`, so it can
never satisfy the exact-args branch, and every non-specialized
instantiation fell through to that fallback. Measured through the real
pipeline: with the primary forward-declared and the specialization
defined first, `Vec<int> vi; vi.save()` emitted `Vec<bool>::save`.
Declaring the primary first gave the correct target — selection was
SOURCE-ORDER DEPENDENT. Two more triggers behaved the same way: a
partial specialization (`Vec<int*>` against `Vec<T*>`), and lexical
shadowing between a global `Box<bool>` and a namespaced `N::Box<bool>`.

Two changes, neither of which is any of the three remediations the
review proposed — each was rejected on measured evidence:

- Exact-argument matching is now LEXICAL-FIRST. Candidates come from the
  scope chain, and the workspace-wide qualified-name bucket is consulted
  only when the chain produced no exact match, so cross-file
  specializations still bind.
- The base-name route refuses a definition that pinned its own template
  arguments: if the fallback's answer carries `templateArguments`, the
  visible candidates are re-decided with those removed — exactly one, or
  decline.

Why not the filed options. "If specializations exist and none matches
exactly, return undefined" deletes a green committed row
(`neg-cpp-specialization/runInt` legitimately resolves to the primary).
"Resolve all defs for the base name, return only on exactly one" deletes
a working edge for C# `partial class Repo<T>` split across files — two
unspecialized defs under one name is legitimate, and
`QualifiedNameIndex`'s own docstring names that case. Preferring the
primary alone fixes nothing about shadowing, which is a ranking bug.

The guard is expressed as `carriesOwnTemplateArguments`, not as
"specialization", so shared pipeline code still names no language
(AGENTS.md R6). It can only fire where a declared name carries concrete
arguments — measured `undefined` for `class Repo<T>` in TypeScript and
C# and for a C++ primary template — so the blast radius is bounded to
C++-style specializations.

Partial-specialization SELECTION is deliberately not implemented:
choosing `Vec<T*>` for `Vec<int*>` needs template-argument deduction,
which is a semantics expansion and cannot live in language-neutral
shared code. The source-order dependence is what is fixed; the answer is
now deterministically the primary.

Also in this commit: dropped an unreachable `?? []` (QualifiedNameIndex
returns a frozen empty array on miss by contract) whose comment was
wrong on both clauses; made the docstring true about argument ERASURE
being what widens what binds, rather than only the decoration stripper;
and corrected a stale pointer that still placed
`resolveClassBindingForName` in `receiver-bound-calls`.

`findClassBindingInScope` itself is untouched — 38 call sites, CRITICAL.

Verified: matrix 56/56, cpp.test.ts 334, unit scope-resolution 1505.
Mutation proof: reverting this file fails the three trigger cases and
passes the non-regression cases; restoring it passes all five.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp

* fix(python): close the deny-set drift axis by case-folding, not by vigilance (#2833)

Review of #2855 found `NOT_A_USER_GENERIC` was a closed list over an
open universe: four review lanes each escaped it with a DIFFERENT set of
names. `Deque` was the sharpest — its lowercase twin `deque` was already
listed, so the omission was an internal inconsistency rather than a
judgement call, and with a workspace `class Deque` present
`self.dq: Deque[User]` fabricated a `Deque.appendleft` edge.

The structural cause is PEP 585: nearly every container has two
spellings differing only in case (`deque`/`typing.Deque`,
`frozenset`/`FrozenSet`). Exact matching forced every pair to be listed
twice, so any half-pair was a silent escape. The deny lookup is now
CASE-FOLDED, which closes that axis by construction — `Deque` becomes
impossible rather than remembered.

`SINGLE_ARG_CONTAINERS` and `MAPPING_CONTAINERS` are now the single
source of truth: they build the two container regexes (verified
byte-identical `.source` and `.flags`, so zero behaviour change) and
feed the property test. The deny set is re-scoped to a closed, auditable
universe — the documented Python stdlib type-system surface — and grew
39 -> 65 concepts: the `collections.abc` views, `contextlib` managers,
`re.Pattern`/`Match`, the `IO` family, ordinary-named stdlib generics
(`Queue`, `Task`, `Future`, `PathLike`), the remaining typing special
forms, and the generic machinery (`Generic`, `Protocol`, `TypeVar`...).

Third-party generics (`Mapped`, `QuerySet`, `Model`) are deliberately
NOT added and are pinned as a decision: that universe is open,
enumerating it only chases the last escape, and declining `Model` would
cost real edges in the many projects that declare one.

The review's suggested property test — derive the names from the
`single`/`dict` regex sources — would NOT have caught `Deque`: `deque`
appears in neither regex, only in the deny set. Both properties are
implemented, since they catch different drift.

The unit test was also TAUTOLOGICAL: it asserted members OF the deny
set, so it structurally could not detect an omission. It now asserts
case-fold closure and PEP 585 alias coverage, and the capture fixture
drops its `as unknown as` cast for the fully-typed helper pattern the
sibling `java-interpret.test.ts` already uses.

Still at interpret time, so no further SCHEMA_BUMP (already 45 -> 46).
Proving the base is a class the FILE can see — the real fix for the
remaining exposure, since `findClassBindingInScope` binds any name with
exactly one workspace def regardless of scope or imports — is a
follow-up, not reachable from this file.

Mutation proof: restoring HEAD's deny-set contents and exact-match
lookup fails four assertions including the `Deque` pair, with the
pre-existing guard rows still passing; restoring gives 125/125.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp

* fix(cpp): capture qualified generic member fields, and make the bench gate see them (#2833)

Review of #2855 found that the three `field_declaration` rules this PR
added only matched a DIRECT `template_type`, so the common real-world
spelling still bound nothing: `std::vector<Item> items;`,
`ns::Repo<User> r;` and `std::unique_ptr<Repo> p;` parse as a
`qualified_identifier` WRAPPING a `template_type`. "C++ fixed" was
overstated.

Six new patterns — three declarator shapes (plain, pointer, reference)
by two qualifier depths — written as separate patterns rather than one
alternation, keeping the tree-sitter 0.21 field-position discipline the
existing rules follow.

The design choice was measured, not assumed. Codex suggested preserving
the full qualified spelling and normalizing `::`; preserving resolves
NOTHING, because `findClassBindingInScope`'s dotted-tail fallback splits
on `.` while C++ writes `::`, and `ns::Repo` is not an index key either
(C++ emits no `@declaration.qualified_name`). Measured: `ns::Repo<User>`
resolves to nothing, `ns.Repo<User>` resolves to `Repo`. Since a
tree-sitter capture is a NODE and not synthesized text, the only lever
is which node to capture — so `@type-binding.type` goes on the INNER
`template_type`, dropping the qualifier and landing on the same
single-match-or-decline path the bare spelling already takes.

Qualifier depth 3+ (`a:🅱️:c::Repo<User>`) remains uncaptured. Stated as
a limit and pinned by a test row, not claimed as fixed.

The bench blindness the review identified is also closed. The
`scope-capture` C++ corpus contained ZERO template-typed member fields —
confirmed a fourth way by applying six demonstrably behaviour-changing
patterns and getting a byte-identical fingerprint. The corpus now
carries generic and qualified-generic members, and the gate is load
bearing for the first time: three states that all hashed to 856d02f3
before now differ (pre-#2833 0e7cbda7, +this PR's 3 rules de07d8b5,
+these 6 rules bd47c82d). Rebaselined for cpp only; c is unchanged.
Histogram diff: only 5 tags move with the fields, each by exactly +40
(20 entities x 2), and every `@reference.*` count is unchanged.

Over-match is preserved: 20 shapes still produce no field capture,
including the 8 original method/pointer/reference/function-pointer/
using/typedef/friend/operator forms plus their `std::`- and
`a:🅱️:`-qualified variants.

Not fixed here, deliberately: NON-generic qualified fields
(`ns::Address addr;`, `std::string name;`) still capture nothing.
Closing that needs six more patterns and would newly bind every
`std::string`/`std::mutex` member repo-wide, changing edges far outside
#2833. Separate issue.

The template-template-parameter hazard the review filed against these
rules is NOT capture-side: a tree-sitter query has no scope knowledge,
so it cannot know `Map` is bound by the enclosing `template <...>`
header, and the PRE-EXISTING `type: (type_identifier)` rule already
captures a bare `T item;` and erases it the same way. It is handled by
the lexical ranking in `walkers.ts` in this series.

Mutation proof: reverting this file fails 9 of 32 assertions (all eight
qualified spellings return no capture) while every over-match negative
still passes; restoring gives ALL PASS. Bench `--check` passes for all
15 languages.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp

* test(resolution): pin specialization order, shadowing and the untested spellings (#2833)

Grows the generic-field matrix 56 -> 114 tests, closing every coverage
gap the #2855 review named and turning the fix-agents' scratch evidence
into permanent rows.

The rows that discriminate against the resolver fix (they fail if
`walkers.ts` is reverted):

- C++ specialization must not depend on DECLARATION ORDER: the
  forward-declared-primary/specialization-first arrangement must land on
  the primary, same as the mirror arrangement. Plus a cross-case
  property asserting the two independently built fixtures agree.
- Partial specialization is deterministic in both orders. The note says
  explicitly that selecting `Vec<T*>` would need argument deduction and
  that flipping this row later is a deliberate expansion, not a
  regression fix.
- Lexical shadowing: the namespace-local `N::Box<bool>` wins for a field
  inside `N`, and the global specialization wins at global scope.

The NON-REGRESSION rows are load-bearing — they are why two of the three
proposed remediations were rejected: cross-file C++ specialization
binding, and C# `partial class Repo<T>` split across two files with the
field in a third (two legitimate unspecialized defs under one name).

Coverage the review found missing: C++ pointer and reference generic
fields (two of this PR's three original rules had ZERO coverage); all
six qualified patterns plus the depth-3 boundary pinned as empty;
TS/C# multi-arg container collision; an anti-vacuity sibling for
`neg-bounded-type-parameter`; Swift/Dart rows restructured so the
ANNOTATION is the only possible source (the old rows gave the field an
initializer of the same generic type and could not tell which resolved);
and cross-file, inheritance/MRO, import-alias, static-member and the
TypeScript module-hoist branch.

Six things were measured and pinned AS MEASURED rather than asserted as
wishes, each flagged in its row note: a static/class-level member emits
nothing for generic AND non-generic alike (a static gap, not a generics
one); a cross-file C++ primary template does not bind while the
cross-file specialization does; `std::unique_ptr<Payload>` types to
`unique_ptr` rather than `Payload` (smart-pointer transparency is not
applied on the qualified path); two same-named C++ specializations in
one file collapse to one node id; and the container-name collision
(`Map<string, User>` binding a workspace `class Map`) is recorded as
INTENDED, since the annotation does name that class.

The `new Set(...)` dedup was kept rather than narrowed: a per-case
surplus-edge sweep measured ZERO duplicate edges anywhere in this file,
Swift included, so the quirk that justified a blanket dedup does not
reproduce. The sweep now pins zero surplus per case, so a real
double-emit fails instead of being absorbed.

The file is deliberately NOT split: four assertions compare cases
against each other, cost is linear in cases, and the 1,800,000 ms
`beforeAll` is kept because the same run measured 271-428 s depending on
host load — a tighter bound converts contention into a red suite. The
reasoning is recorded in the file header.

Also corrects the SCHEMA_BUMP pin-test title, which still said (#2766).

Mutation proof: reverting `walkers.ts` fails exactly the five order and
shadowing assertions and passes the other 109; restoring gives 114/114.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp

* feat(resolution): capture declared type parameters so a type variable is not a class (#2833)

Three review findings were blocked on one missing fact. `templateArguments`
records the arguments a declaration was written AGAINST (`struct Vec<bool>`);
nothing recorded the parameter list a declaration DECLARES (`template <class T>`,
`class Box<T extends Repo>`). So the resolver could not tell a type variable
from a class, and:

- `class Box2<T> { t: T }` beside a workspace `class T` emitted a FALSE edge
  `run2 -> T.foo`. `T` carries no type arguments, so it never entered the
  generic branch — the plain lookup simply bound a same-named class. The
  lexical grounding added elsewhere in this series cannot help, because
  `export class T` IS lexically bound.
- `class Box<T extends Repo> { t: T }` resolved to nothing: no recorded bound
  to resolve through.
- A full specialization `template<> struct Vec<T*>` and a partial
  `template<class T> struct Vec<T*>` were byte-identical (`['T*']`).

`SymbolDefinition.typeParameters` now records `{ name, bound? }` in declaration
order (substitution is positional). `bound` is kept verbatim and un-split, so
`Repo & Closeable` stays whole; ABSENT means UNKNOWN, never "unbounded", which
is what keeps unconverted languages behaving exactly as before.

Transport is the raw parameter-list node via `@declaration.type-parameters`,
read by a language-neutral parser that recognizes TOKENS, not languages:
`extends`/`:` introduce a bound, the name is the trailing identifier, so
`class T`, `typename T`, `in T`, `out T`, `reified T` and `class... Ts` are one
rule. Populated for TypeScript, C++, Java, Kotlin, C# and Rust. JavaScript, C,
COBOL, PHP and Ruby have no declared type parameters to capture; Go and Python
spell them with SQUARE brackets, which this parser deliberately rejects as
ambiguous against subscript and array spellings (Go already has a working
main-thread sidecar in this series); Dart and Swift are straightforward
follow-ups.

Two latent hazards found and closed on the way:

- The new capture was not in `KNOWN_SUB_TAGS`, so it could out-span its own
  declaration and become the anchor — silently DROPPING the whole class def.
- A templated C++ struct matches both the standalone and `template_declaration`
  patterns, minting two defs under one id, and only one twin could see the
  parameter list. `buildDefIndex` is first-write-wins, so MATCH ORDER decided
  whether `Vec` remembered `T`. A narrow duplicate-declaration backfill gives
  both twins the list.

Also fixed by its own test: a Rust lifetime `'a` parsed as a parameter named
`a`, which would have shadowed a real class.

Parse-time output lands in the cached ParsedFile, so SCHEMA_BUMP goes 46 -> 47.
Re-checked against origin/main at write time: main is on 45; 46 was taken by
this same branch, and a warm cache stamped 46 carries ParsedFiles with no
`typeParameters` at all.

The csharp and rust capture goldens were regenerated with the tests' own
documented `UPDATE_GOLDEN=1`; only digests moved, no captureGroups.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp

* fix(resolution): ground erased base names, and stop a class name from being enough (#2833)

The review's central risk was that this PR converts MISSING edges into
CONFIDENTLY WRONG ones. Base-name erasure (`Repo<User>` -> `Repo`,
`Repo[User]` -> `Repo`) bound through a workspace-wide qualified-name
fallback that consults NO scope, NO import and NO module — it bound any name
with exactly one workspace def. That is why a Python `Mapped[User]` could bind
an unrelated `class Mapped`, and why the language deny lists were papering
over an open universe.

`resolveErasedBaseName` now admits an erased base on one of four grounds,
strongest first: the scope chain binds it; the declaration is in the SAME
FILE; the index proves the name is a template family; or the file binds no
cross-file class at all, so its silence is no evidence. The last ground fails
toward permissive on purpose — every way it can be wrong costs a wrong edge
that already existed, never a working one.

Two measurements drove that design and refuted the simpler rule. A C++
`#include` materializes NO binding whatever, and C# resolves cross-namespace
without `using` through the index — so a pure "require lexical grounding" rule
would have deleted every cross-file C++ generic member. Both are now pinned.

Python erases at CAPTURE time, so by resolution there is no `<` and the
grounded route was never entered. `erasedTypeApplication` rebuilds the
application from `TypeRef.declaredSpelling` — strictly: the raw name must be
the base and the argument list the whole balanced remainder, so `User[]`,
`vector<Item>` and `Repo<User>?` decline and behave exactly as before.

Closing it took finding FOUR emitters, not one. Three were in Case 4; the
fourth was `emitReferencesViaLookup` re-emitting the refused edge from the
pre-resolved reference index, which needed the site marked handled with a
recorded `receiver-unresolved`. A fifth lived in the text cascade: a declined
fold falls THROUGH by design, and the cascade held its own ungrounded copy of
the member-typing lookup. This file typed a receiver from a `TypeRef` in five
places and the PR had wired three; all five now go through one
`classOfDeclaredType`.

Also here, from the same review:

- Type parameters no longer bind a same-named class (uses the new
  `typeParameters`), and a BOUNDED parameter resolves through its bound.
- A cross-file C++ PRIMARY template now binds: a ranking bug, not a capture
  one — the index fallback needs exactly one candidate and `Vec` held two, so
  removing the argument-pinned declaration leaves one.
- `this->field.m()` resolved to nothing for generic AND non-generic alike. A
  language that declares `this` IS the enclosing class
  (`resolveThisViaEnclosingClass`) synthesizes no `this` typeBinding, so a
  chain whose BASE is `this` could never seed its head. Reading the provider
  flag keeps the rule language-free.
- Class-level (static) member receivers emit nothing in TypeScript and Kotlin
  — for the non-generic control too. Case 6 types them from the DEF side
  (`isStatic` + `declaredType` on the field node), which needs no capture
  change; the target lookup stays the ordinary instance walk, so a static
  field HOLDING an instance still binds an instance method and a genuine
  static call is untouched.

Partial-specialization SELECTION is deliberately not implemented: it needs
argument deduction against a parameter list, and full C++ partial ordering is
a real algorithm with no measured driving case. The discriminator now exists
if someone wants it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp

* fix(cpp,js,php,go): close the remaining per-language generic-field gaps (#2833)

Four language gaps the review measured, each with a different cause.

**C++ qualified member fields.** `std::vector<Item> items;`, `ns::Repo<User> r;`
and `ns::Address addr;` captured NOTHING: every field rule required the type
node to BE a `type_identifier` or `template_type`, and a qualified member type
is neither — tree-sitter wraps both in a `qualified_identifier`. Three
depth-agnostic rules (one per declarator shape) now match the outer node, which
also REMOVES the depth boundary rather than raising it: depths 1-4 capture,
generic and non-generic alike.

Preserving the qualifier resolves nothing — measured: `ns::Repo<User>` binds
neither way, because the dotted-tail fallback splits on `.` while C++ writes
`::`, and `ns::Repo` is not an index key. Since a capture is a NODE and not
synthesized text, the qualifier is dropped in `interpret.ts` by a top-level-only
`::` split, so `std::vector<std::string>` reduces to `vector<std::string>`, not
`string`.

Measured cost of the non-generic half, which was the reason to hesitate: field
captures go 8 -> 32 across the C++ bench corpus, but the resolution-level census
over those 13 repos is 32 CALLS edges before and 32 after, BYTE-IDENTICAL. It
fabricates only where a workspace class shares a std name (`class string` beside
`std::string name;`), which is the same accepted policy the already-landed
qualified-generic rules carry, pinned in the matrix as intended.

**JavaScript `@type {Repo<User>}` and PHP `@var Repo<User>`.** Neither bound a
field type — and neither did the NON-generic control, so this was a docblock gap
rather than a generics one. PHP needed TWO captures, not one: with only the type
binding, `$this->repo->save()` resolved until a second class declared `save` and
then went unresolved, because narrowing a same-named method needs the receiver's
member owned. Generics do NOT come free in PHP — `normalizePhpType('Repo<User>')`
returns `'User'` by the container-element convention, so passing the raw spelling
through would have emitted `User::save`; type arguments are erased at capture
instead. In JavaScript they DO come free, verified byte-identical to the
TypeScript control. Both decline what they cannot prove: arrays, `list<User>`,
unions, `Promise`/`Array` wrappers (via an exported predicate rather than a
copied name list), statics, and any property that already has a native type.

**Go generic interfaces.** `UserRepo` genuinely DOES implement `Repo[User]` —
the spec says a generic type must be instantiated, that instantiation
substitutes type arguments and yields a new non-generic type, and that a type
implements an interface when it is in its type set. So the old behaviour was a
FALSE NEGATIVE and the matrix note calling it "already correct" was wrong.
Satisfaction is now checked against POSITIONALLY SUBSTITUTED method sets, so
`Repo[Order]` does not match a `Save(x User)` implementor — substitution, not
erasure. #2829's exact method-set model is untouched: pointer receivers still
follow MS(*T), unexported names stay package-scoped, the declaration's own
method set is still checked first, and the harvest is gated so a repo with no
generic interface never runs it. `go.test.ts` is unchanged at 296 passing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp

* test(resolution): pin every fix from the review, 114 -> 155 rows (#2833)

Eight rows in this matrix pinned gaps that the fixes in this series close, so
each asserted the opposite of the new truth. All eight are flipped, and the
prose describing them as open gaps is corrected. Nine new cases cover the fixes
that would otherwise have shipped unpinned.

Flipped, each measured: the type-parameter FALSE edge (`run2`) is gone; a
bounded parameter now resolves through its bound with fan-out; the cross-file
C++ primary binds; the C++ qualifier depth boundary is removed rather than
raised; Go gains its two structural implementors and JOINS the paired sweep,
which had quietly excluded it — that exclusion was the taxonomy admitting a bug;
and both static-member rows resolve.

Added: JS `@type` and PHP `@var` docblock fields with three PHP declines; a
Kotlin `companion object` receiver (given an INTERFACE control so the paired
sweep can check it, which `ts-reach-shapes` cannot — its two sides are not
count-comparable); the Python third-party grounding refusal plus the ground that
still ADMITS, so an empty row can never be read as "erased names never resolve";
the four mirrors that would break if grounding were tightened (same-file and
imported Python, a C++ `#include`, C# cross-namespace without `using`); C++
qualified non-generic fields including the fabrication policy and its absence
case; `this->field.m()` for generic and non-generic with bare controls; and a Go
negative proving substitution is positional, not erasure.

Three shapes are pinned AS MEASURED with notes saying they are deliberate limits
so nobody "fixes" them by accident: C++ partial-specialization selection is
deterministically the primary (real selection needs argument deduction);
`std::unique_ptr<T>` types to the pointer, not the pointee (`.` and `->` are
indistinguishable to the resolver, so transparency would trade a recoverable
miss for a confident wrong edge); and two same-named C++ specializations in one
file collapse to one node id, which is why the shadowing fixture uses two files.

One row pins a REMAINING wrong edge rather than hiding it: `m.inner.ping()` on
a `Mapped[User]` head still binds the unrelated workspace class, while the
one-segment-shallower `m.save(u)` correctly declines. The obvious one-line guard
was written and MEASURED not to close it, so the surviving route is elsewhere
and wants its own diagnosis — a broader refusal would change chain-head
resolution for every language without pinning the shape it is meant to fix.

`bench/scope-capture` is rebaselined for the six languages whose captures moved,
regenerated from a fresh measurement rather than pasted; `--check` passes for all
15.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp

* perf(resolution): remove three measured hot-path regressions this series added (#2833)

A quality pass over the #2833 series found three performance defects it had
introduced, all measured, plus dead code and stale docs from six agents having
appended to the same files across four rounds. No behaviour change: the
resolver suite is identical before and after, and every scope-capture
fingerprint is byte-identical.

**An accidental quadratic in Go instantiation harvesting.** `collectGoInstantiations`
calls `record()` for every type binding and every declared, return and parameter
type in every Go file, and the `includes('[')` gate does not filter Go's most
common types — `map[string]string`, `[]map[string]*v1.Pod` and
`map[string]map[string]int` all produce a `map` candidate. Each false base then
failed a full scope-chain walk and fell through to a LINEAR SCAN OF EVERY
INTERFACE IN THE PROGRAM, with no dedupe on the spelling, so the same
`map[string]string` written 10,000 times paid 10,000 scans. Now a
qualified-name index built in `buildDetectionIndexes` (one probe, ambiguity
semantics preserved exactly) plus a per-scope base memo:

    8,000 interfaces / 80,000 spellings:  6,662 ms -> 104 ms   (64x)

`resolveEmbeddedInterface` held a byte-identical copy of that scan and now
shares the helper. `GoInstantiation` was a single-field wrapper and collapses
to the array it wrapped; its two parallel maps fold into one whose inner key IS
the dedupe. `candidateStructIdsFor` was rebuilt per instantiation although
every substituted method set has the same key set — hoisted, and materialized,
because one branch returned a live iterator that would have yielded nothing on
a second pass.

**`scanForCrossFileClass` asked a name-keyed question that needs no name key.**
It answered "does this file bind any cross-file class" by probing every
accessible namespace once PER NAME. It now iterates the channels directly,
taking whichever side is smaller so a large namespace table cannot reintroduce
the product. Predicate and early exit preserved:

    5,000 module names x 1,000 namespaces:  159.0 ms -> 1.2 ms   (132x)

**A duplicated scope walk on every generic receiver.** `resolveClassBindingForName`
computed the lexical candidate list, then `resolveErasedBaseName` recomputed
the identical `findAllBindingsInScope`. Computed once and passed:

    receiver at depth 8:  5,617 ns -> 3,091 ns   (-45%)

**A whole extra AST traversal per JavaScript and PHP file.** The docblock
synthesis passes each added a full tree walk to find one node kind — the ninth
in the JS emitter, the third in PHP. `node.namedChildren` materializes a
wrapper array across the N-API boundary for every node, so one added pass cost
1.9x what parsing the entire file costs. Folded into the existing walks as one
more node kind; capture output is byte-identical and every fingerprint is
unchanged. Total emit time per file drops 4-7%.

Hygiene, all verified stale rather than assumed:

- `receiverOriginOpts` passed `resolveThisViaEnclosingClass`, which
  `classifyReceiverOrigin` never reads — the "both hooks" comment above it is
  true again.
- The `stripDecoration` docstring's caller roll-call claimed the only
  edge-emitting caller "emits no edge and can only change a diagnostic label".
  Case 6 passes it and does emit edges. Replaced the roll-call with the rule;
  six rounds each appending a name to a list is how it went wrong.
- A Python comment described the resolution-time grounding as a follow-up that
  "this parse-time pass cannot do" — it landed in this same branch and is
  pinned by `py-erased-grounding`.
- `classOfDeclaredType` took a `scopeId` all five callers derived from the
  `TypeRef` they also passed. Dropped, so "these five are the same call" is
  enforced rather than asserted.
- Three exports with no consumer outside their own file.
- PHP had three copies of one preceding-comment sibling walk and two regexes
  for one tag, so a fix to either reader of `@var` would land on one and not
  the other — the symptom being a field typed differently from its own foreach
  element type. One walk, one regex.

Tests: the new matrix leaked a fixture repo per case; it now carries the
sibling suite's `cleanupTempDirSync` and the Windows EBUSY reasoning that goes
with it. `PAIRED` was a second hand-maintained list and 19 of 41 cases had
silently fallen out of it — it is derived from the cases now, with a new
assertion that each case is either swept as a pair or carries a written reason
it is not. That recovered one genuine omission (`php-typed-property`).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KtNfG6EPn738Y51AYs7wDp

* test(bench): rebaseline receiver-resolution for the #2833 this-> fix

The `Receiver-resolution drop guards` CI step failed on this branch:

  shapeArm.cpp.fieldReceiverCall:  "INVISIBLE-GAP" -> "RESOLVES"
  shapeArm.cpp.decoratedFieldType: "INVISIBLE-GAP" -> "RESOLVES"

Both are the intended improvement. The guard is exact-match by design —
the drop count cannot move without a deliberate rebaseline, and the
rebaseline path demands the movement be explained — so this records the
two shape flips and leaves the call-drop count arm untouched.

BASELINE.md still claimed `this->repo.save()` and `this->repo->save()`
were INVISIBLE-GAP. That is now false: the `resolveThisViaEnclosingClass`
head seed added in this PR resolves both. Also notes what the control
established — this was never a generics gap, since the non-generic
control failed identically before the fix.

* docs(parse-cache): narrow the SCHEMA_BUMP ledger to what the bump delivers

The ledger claimed a warm cache would make "the whole fix ... a silent
no-op on every incremental analyze". That overstates the constant. The
bump invalidates the PARSE half; whether the re-parsed captures reach the
graph is gated separately and does not move:

  - `isIncremental` (core/run-analyze.ts) tests `!options.force`, an
    existing meta, `!schemaFingerprintMismatch(...)`, feature parity,
    non-empty `fileHashes` and a git repo. SCHEMA_BUMP is in none of them.
  - the incremental branch writes back only `hashDiff.toWrite` and logs
    the rest as "unchanged file rows preserved".
  - SCHEMA_FINGERPRINT hashes node/relation DDL, untouched here, so it is
    byte-identical and moves nothing either.

So an incremental analyze re-parses an unchanged file correctly but keeps
its existing rows; the new edges land on the next full rebuild. That is
the pre-existing contract for every capture change, not a regression in
this PR — but the comment should not promise more than it delivers.

Comment only; no behavior change. SCHEMA_BUMP stays 48.

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 17:14:13 +01:00
.agents/plugins feat: full Codex support — hooks, plugin marketplace, and setup (#2328, supersedes #1131) (#2369) 2026-07-04 13:32:17 +01:00
.claude feat(cli): add a bunx lane so bun-only machines can run gitnexus (#2765) 2026-08-06 08:40:36 +00:00
.claude-plugin chore: release v1.6.9 (#2367) 2026-07-04 07:53:06 +01:00
.cursor fix(storage): rename index metadata to gitnexus.json with dual-write compatibility (#2363) 2026-07-03 19:32:59 +01:00
.devcontainer fix: large-repo analyze OOM and false worker-timeout cascade (#2649) (#2679) 2026-07-25 09:16:17 +01:00
.gemini/commands feat(review): add PR reviewer swarm agents (#1851) 2026-05-29 18:24:16 +01:00
.github chore(deps): bump docker/login-action from 4.4.0 to 4.6.0 (#2851) 2026-08-06 08:19:19 +01:00
.history/gitnexus fix(test): add --repo to CLI e2e tool tests for multi-repo environment 2026-03-18 08:12:25 +00:00
.husky feat: configure prettier with pre-commit hook (#563) 2026-03-28 14:58:04 +00:00
.sisyphus/drafts fixed constructor to method relation not getting stored in kuzu issue 2026-01-26 22:58:15 +05:30
deploy/kubernetes ci(docker): mirror signed images to Docker Hub alongside GHCR (#1029) 2026-04-23 18:59:26 +01:00
docs/plans fix(python): resolve calls through an unaliased dotted namespace import (#2826) (#2828) 2026-08-05 11:35:27 +01:00
Documentation Add Kilo Code + GitNexus MCP setup guide (#2259) 2026-07-02 11:04:22 +01:00
eslint-rules fix(windows): 32767-char tree-sitter crash + VECTOR extension SIGSEGV (#1433) 2026-05-10 16:00:36 +01:00
eval chore(deps): bump aiohttp in /eval in the uv group across 1 directory (#2825) 2026-08-04 09:55:59 +00:00
gitnexus fix(resolution): resolve calls through a generic-typed field receiver in every language (#2833) (#2855) 2026-08-07 17:14:13 +01:00
gitnexus-claude-plugin feat(cli): add a bunx lane so bun-only machines can run gitnexus (#2765) 2026-08-06 08:40:36 +00:00
gitnexus-cursor-integration fix(analyze): replace the hand-incremented schema version with a derived DDL fingerprint (#2798) (#2808) 2026-08-03 15:04:30 +01:00
gitnexus-shared fix(resolution): resolve calls through a generic-typed field receiver in every language (#2833) (#2855) 2026-08-07 17:14:13 +01:00
gitnexus-test-setup feat: merge gitnexus-mcp into gitnexus package - unified CLI+MCP 2026-02-04 01:12:41 +05:30
gitnexus-web chore(deps)(deps): bump @tailwindcss/vite in /gitnexus-web (#2845) 2026-08-06 09:24:16 +01:00
pr-swarm-review feat(skills): GitNexus Engineering Tool Kits (#2566) 2026-07-19 15:07:24 +01:00
.cursorrules docs: agent development framework, GitHub templates, eval refactor (#479) 2026-03-25 06:48:41 +00:00
.dockerignore fix(ci): Change docker base image from alpine to debian (#1014) 2026-04-21 21:31:58 +01:00
.env.example feat(analyze): private GitHub repos via PAT + Azure DevOps Server support (#2076, #2210) (#2223) 2026-06-16 05:49:02 +01:00
.git-blame-ignore-revs feat: configure eslint with unused import removal (#564) 2026-03-28 15:28:09 +00:00
.gitattributes feat(devcontainer): add devcontainer for Claude/Codex/Cursor CLIs (#1875) 2026-06-02 05:09:01 +01:00
.gitignore chore: stop tracking docs/plans (planning output stays local) 2026-07-21 10:09:35 +00:00
.gitleaks.toml feat(analyze): private GitHub repos via PAT + Azure DevOps Server support (#2076, #2210) (#2223) 2026-06-16 05:49:02 +01:00
.gitleaksignore chore(security): suppress deleted auth placeholder 2026-07-16 10:08:59 +07:00
.mcp.json fix: use cross-platform npx command in .mcp.json 2026-02-22 17:46:04 +00:00
.prettierignore chore(quality): exclude test/fixtures from CodeQL, ESLint, and Prettier (#1313) 2026-05-04 09:35:34 +01:00
.prettierrc feat: configure prettier with pre-commit hook (#563) 2026-03-28 14:58:04 +00:00
.windsurfrules resources implemented and agents.md and skills updated to use it 2026-02-05 05:13:48 +05:30
AGENTS.md feat(cli): add a bunx lane so bun-only machines can run gitnexus (#2765) 2026-08-06 08:40:36 +00:00
ARCHITECTURE.md fix(resolution): resolve Go pointer-receiver calls, and report the program boundary instead of hedging (#2766) (#2782) 2026-08-01 22:42:18 +01:00
CHANGELOG.md perf(hooks): cmdline-first Linux db-lock scan, drop the lsof fallback (#2180) (#2183) 2026-06-13 11:52:14 +01:00
CLAUDE.md feat(cli): add a bunx lane so bun-only machines can run gitnexus (#2765) 2026-08-06 08:40:36 +00:00
compound-engineering.local.md feat: Phase 7 type resolution — return-aware loop inference & PHP class-property iterables (#341) 2026-03-18 08:39:38 +00:00
CONTRIBUTING.md feat(serve): validate and port-scope the origin/proxy configuration surface (#2820) 2026-08-05 06:52:39 +01:00
docker-compose.yaml feat(web): support GITNEXUS_BACKEND_URL env var for Docker deployments (#1286) 2026-05-25 11:21:11 +01:00
docker-server.mjs feat(render): add one-click deploy to render support (#2804) 2026-08-06 00:19:44 +00:00
docker-server.test.mjs feat(render): add one-click deploy to render support (#2804) 2026-08-06 00:19:44 +00:00
Dockerfile.cli feat(render): add one-click deploy to render support (#2804) 2026-08-06 00:19:44 +00:00
Dockerfile.web fix(security): Pin Docker Node base images, remove runtime package-manager CVE surface, verify Trivy on PRs, and harden Dependabot policy (#1455) 2026-05-09 16:55:31 +01:00
DoD.md feat(skills): GitNexus Engineering Tool Kits (#2566) 2026-07-19 15:07:24 +01:00
eslint.config.mjs fix(windows): 32767-char tree-sitter crash + VECTOR extension SIGSEGV (#1433) 2026-05-10 16:00:36 +01:00
GUARDRAILS.md fix(analyze): gate FTS-indexed DML before the incremental writeback (#2841) (#2854) 2026-08-07 09:44:44 +01:00
LICENSE docs: update license copyright holder 2026-02-03 22:54:01 +05:30
llms.txt docs: agent development framework, GitHub templates, eval refactor (#479) 2026-03-25 06:48:41 +00:00
MIGRATION.md fix(analyze): replace the hand-incremented schema version with a derived DDL fingerprint (#2798) (#2808) 2026-08-03 15:04:30 +01:00
package-lock.json chore(deps-dev): bump the npm_and_yarn group across 1 directory with 2 updates (#2621) 2026-07-21 22:43:34 +01:00
package.json feat(package): add gitnexus commands for analysis 2026-04-29 16:37:06 +03:00
README.md feat(render): add one-click deploy to render support (#2804) 2026-08-06 00:19:44 +00:00
render.yaml feat(render): add one-click deploy to render support (#2804) 2026-08-06 00:19:44 +00:00
RUNBOOK.md fix(embeddings): retry unparseable 200 responses and survive partial embedding failures (#2790) (#2795) 2026-08-02 20:43:59 +00:00
SECURITY.md feat(render): add one-click deploy to render support (#2804) 2026-08-06 00:19:44 +00:00
skills.mdm FEAT: Added support for optional skill generation based on KuzuDB after initial repo analysis (npx gitnexus analyze --skills) (#171) 2026-03-13 08:29:13 +00:00
swift-ingestion-gaps.md docs: add macro declarations to Swift ingestion gaps 2026-03-23 14:21:32 +01:00
TESTING.md refactor(ingestion): delete legacy call-resolution DAG + heritage processor (RING4-1, #942) (#2023) 2026-06-04 11:07:37 +01:00
type-resolution-roadmap.md feat: implement cross-file binding propagation for multiple languages 2026-03-21 07:47:04 +00:00
type-resolution-system.md feat(ingestion): TypeScript registry-primary scope resolution (Ring 3) (#1050) 2026-04-26 08:23:08 +01:00

GitNexus

⚠️ Important Notice: GitNexus has NO official cryptocurrency, token, or coin. Any token/coin using the GitNexus name on Pump.fun or any other platform is not affiliated with, endorsed by, or created by this project or its maintainers. Do not purchase any cryptocurrency claiming association with GitNexus.

abhigyanpatwari%2FGitNexus | Trendshift

Discord npm version License: PolyForm Noncommercial OpenSSF Scorecard CI Workflows

The nervous system for agent context.

Indexes any codebase into a knowledge graph — every dependency, call chain, cluster, and execution flow — then exposes it through smart MCP tools so AI agents never miss code.

💬 Discord · 🌐 Web UI · 🏢 Enterprise (SaaS & self-hosted)

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

# 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:

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

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

Fastest MCP startup: install globally (npm i -g 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 -g 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.

Behind an HTTP proxy / regional firewall? onnxruntime-node's postinstall downloads optional CUDA binaries from api.nuget.org and ignores HTTP_PROXY/HTTPS_PROXY (#2370). The embedding stack is an optional dependency, so a failed download no longer breaks the install — and it self-heals: the first gitnexus analyze --embeddings (or gitnexus embeddings install) fetches the stack through your npm registry config (mirrors/proxies apply, no NuGet) into ~/.gitnexus/embedding-runtime (override with GITNEXUS_EMBEDDING_RUNTIME_DIR). The on-demand prefix needs Node with module.registerHooks (≥ 22.15 on 22.x, ≥ 23.5 on 23.x); on older Node, keep the stack in the install itself with ONNXRUNTIME_NODE_INSTALL=skip npm install -g gitnexus (works on every supported Node).

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:

Deploy to Render

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.

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 -g 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:

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

17 MCP tools (15 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)
group_list List configured repository groups
group_sync Rebuild a group's Contract Registry and cross-repo links

Per-repo tools take an optional repo parameter (omit it when only one repo is indexed) and an optional branch for indexes pinned with gitnexus analyze --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
OpenCode Yes Yes MCP + Skills
CodeBuddy (Tencent) Yes Yes MCP + Skills
Qoder (Alibaba) Yes Yes MCP + Skills
Windsurf Yes MCP

Claude Code and Codex get the deepest integration: MCP tools + agent skills + PreToolUse hooks that enrich searches with graph context + PostToolUse hooks that detect a stale index after commits and prompt the agent to reindex.

¹ 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):

# 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):

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

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

[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):

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.

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

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

Antigravity (Google) — ~/.gemini/antigravity/mcp_config.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):

{
  "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:

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

Qoder (Alibaba) — ~/.qoder.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:

gitnexus setup                   # Configure MCP for detected editors (one-time; -c to select)
gitnexus analyze [path]          # Index a repository (or update a stale index)
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.

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.

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
gitnexus analyze --force         # Full rebuild: re-parse + graph rebuild + FTS rebuild
gitnexus analyze --repair-fts    # Fast path: rebuild/verify only FTS indexes on existing index data
gitnexus analyze --skills        # Generate repo-specific skill files from detected communities
gitnexus analyze --skip-embeddings  # Skip embedding generation (faster)
gitnexus analyze --embeddings [limit]  # Enable embedding generation (slower, better search)
gitnexus analyze --skip-agents-md   # Preserve custom AGENTS.md/CLAUDE.md gitnexus section edits
gitnexus analyze --skip-skills      # Skip installing standard skill files under .claude/skills/ and .agents/skills/
gitnexus analyze --skip-git         # Index folders that are not Git repositories
gitnexus analyze --default-branch develop  # Branch used in the generated regression-compare example (base_ref)
gitnexus analyze --verbose       # Log skipped files when parsers are unavailable
gitnexus analyze --worker-timeout 60  # Increase worker idle timeout for slow parses
gitnexus analyze --workers <n>   # Parse worker pool size (>=1; default: cores-1, capped at 16,
                                 # auto-sized to the repo). 0 is rejected — there is no sequential mode.
gitnexus analyze --wal-checkpoint-threshold 67108864  # LadybugDB WAL auto-checkpoint threshold in bytes
                                 # (default 67108864 = 64 MiB; -1 keeps Ladybug stock ~16 MiB)

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 limitgitnexus analyze --embeddings generates semantic search vectors with a default 50,000-node safety cap to protect memory on large repositories:

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.

Repository groups (multi-repo / monorepo service tracking)
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.

{
  // 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
  "workerTimeout": 60,
}

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

{ "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, embeddingThreads, embeddingBatchSize, embeddingSubBatchSize, embeddingDevice.
  • The file is JSON only. Unknown keys and invalid values fail fast with an actionable error before analysis starts.
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 env vars; env vars take precedence over built-in defaults.

Variable Default Effect Tune when…
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_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_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_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_WAL_CHECKPOINT_THRESHOLD 67108864 (64 MiB) LadybugDB WAL auto-checkpoint threshold in bytes. Equivalent to --wal-checkpoint-threshold <bytes>. -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) Chunk boundary used for cache-key composition and dispatch. Smaller = finer-grained cache hits but more dispatch overhead. Tuning incremental-analyze cache behavior on monorepos.
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.
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++
Dart

Imports — cross-file import resolution · Named Bindingsimport { 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 (portable, gitignored) and registers a pointer in ~/.gitnexus/registry.json. When an AI agent starts, the MCP server reads the registry and can serve any indexed repo. LadybugDB connections are opened lazily on first query and evicted after 5 minutes of inactivity (max 5 concurrent). If only one repo is indexed, the repo parameter is optional on all tools — agents don't need to change anything.

Architecture diagram
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

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:

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
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)

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)

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)

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

-- 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:

# 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

# 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.

gitnexus_img

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
git clone https://github.com/abhigyanpatwari/gitnexus.git
cd gitnexus/gitnexus-shared && npm install && npm run build
cd ../gitnexus-web && npm install
npm run dev
# Then in another terminal, start the backend the frontend connects to:
npx gitnexus@latest serve

Docker

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:

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
# 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):

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.
  • 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 gitnexus@1.6.2 — 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:

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):

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:

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.

# 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 founders@akonlabs.com

Community Integrations

Built by the community — not officially maintained, but worth checking out.

Project Author Description
pi-gitnexus @tintinweb GitNexus plugin for pipi 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. Index stored in .gitnexus/ (gitignored). 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

Star History Chart

Acknowledgments