Find a file
Gergő Magyar 4906daf27b
fix(scope-resolution): resolve calls through a closure-valued binding across languages (#2693) (#2695)
* fix(scope-resolution): resolve calls through a closure-valued binding (#2693)

`val f = { }; f()` emitted no CALLS edge in Kotlin or Swift, so `impact` on
such a symbol under-reported to zero — the same false all-clear as #2687.

The cause was not, as first suspected, that these languages fail to feed
`callable-value-flow`. They do: `synthesizeCallableFlowCaptures` is called
from 15 language capture modules, and Kotlin already resolves reassignment
through the pass (`var f = ::a; if (c) f = ::b; f(1)` reaches both targets).
Their captures are already exactly right — the seed names the binding as its
own callable, per the anonymous-callable convention in
callable-flow-captures.ts.

They died one layer later, at the `buildGraphTargetIndex` gate:

    if (!isCallable(def) && providerTarget?.(def) !== true) continue;

`isCallable` is Function/Method/Constructor, but the scope-resolution layer
declares a closure binding with its VALUE label (Kotlin/Swift `Property`),
and `isCallableValueTarget` is implemented by exactly one provider — COBOL.
So the binding never entered `graphTargets`; `lexicalCallableLookup` then
returned `shadowed: true` with no targets, which also suppressed the
workspace-wide fallback, and the seed resolved to nothing.

Only the graph knows a value binding holds a callable — since #2687 it emits
a single `Function` node for one. So value bindings now resolve their graph
id first and are admitted on the label of the node they actually reach.

This is self-limiting: a genuine constant keeps its own Const/Property node,
so `resolveDefGraphId`'s qualified key hits before the label-agnostic
`simpleKey` fallback can reach a same-named callable. Only a binding whose
own value node was replaced by a callable one gets through.

No scope kind changes — Kotlin's `lambda_literal` stays `@scope.block`, so
#1757 smart-cast semantics are untouched by construction. The fix is
language-neutral: it discriminates on the graph node label, never on a
language name.

Dart is fixed separately; its root cause is independent.

* fix(dart): resolve calls through a closure-valued binding (#2693)

Dart needed more than the shared gate fix: neither of its closure-binding
forms could resolve, for two different reasons, and the plan's one-line
diagnosis turned out to be incomplete.

TOP-LEVEL `var f = (x) => x;`
  A graph Function node already existed (#2687), but no `@declaration.*`
  matched the binding, so scope resolution had no SymbolDefinition to attach
  a flow seed to. Adding the declaration exposed a second problem: Dart's
  `initialized_identifier` is FIELDLESS, so the shared field-based assignment
  fallback (`left`/`name`/`value`/…) decomposed nothing and the binding still
  emitted no flow captures at all. Kotlin's fieldless `assignment` node hit
  exactly this and took the same remedy — a provider `extractAssignment`.

FUNCTION-LOCAL `void m() { var f = (x) => x; }`
  Locals parse as `initialized_variable_definition`, which the top-level
  graph-node rules are deliberately anchored under (program) to avoid, so a
  local closure had no graph node at all — nothing for the widened
  `buildGraphTargetIndex` gate to admit.

Both new rules are restricted to a `function_expression` value. Declaring
every Dart variable would mint defs and nodes repo-wide for no resolution
benefit; ordinary locals stay unindexed exactly as before. The top-level
declaration reuses the (program) anchor the graph-node query already relies
on, so class-body fields — which share `initialized_identifier_list` and are
already `@declaration.property` — are never matched twice.

Also drops the now-false note in tree-sitter-queries.ts claiming `f()` does
not resolve for Dart. That node is now the evidence that makes it resolve.

* docs(scope-resolution): document the callable-flow capture contract (#2693)

The module is 1200+ lines behind a nine-line docblock, and the only worked
example was C. Both root causes fixed in this series were "the contract was
discoverable only by reading the emitter":

  - the anonymous-callable convention (a seed whose source is a closure takes
    its DESTINATION's name) is what makes closure bindings resolvable at all,
    and is the reason the widened target gate is correct;
  - a fieldless binding node silently decomposes to nothing under the shared
    assignment fallback, which cost Kotlin one debugging cycle in #2522 and
    Dart another here;
  - captures alone are never enough — the bound name also needs a
    `@declaration.*` or there is no cell to key the seed on.

Records the cell/site model, both traps, and points at the fullest and
smallest worked examples.

Bumps INCREMENTAL_SCHEMA_VERSION 15 → 16 and the parse-cache SCHEMA_BUMP
22 → 23: this series emits NEW CALLS edges and new Dart Function nodes, and
the incremental write set only covers changed files, so an existing index
would keep reporting a zero blast radius for exactly the symbols the fix is
about.

* perf(scope-resolution): pre-filter value bindings in the callable target index (#2693)

Widening the `buildGraphTargetIndex` gate to consider VALUE bindings put the
hot loop on a much larger def population — value bindings outnumber callables
in real source — and the naive version paid full price per binding. Measured
on a synthetic 800-file corpus (8 value bindings per file, 1 of them a closure
binding), the widening cost 2.50-2.82x the pre-#2693 callable-only build.

Two wastes, both provable rather than guessed:

1. `definitionAnchorKey` ran for every def, including value bindings. The
   anchor index is keyed by callable LABEL and the key is built from
   `def.type`, so a value def can never hit it — and the key costs a regex
   per def.

2. Every value binding paid the whole `resolveDefGraphId` key chain only to be
   rejected. It need not: every qualified key that function tries embeds
   `def.type`, so for a VALUE def those can only ever reach a value-labelled
   node. Its one route to a callable is the label-agnostic
   `simpleKey(filePath, simpleName)` fallback, which by construction requires
   a callable node with the SAME file and simple name. So a value binding with
   no such node cannot resolve to a callable, and one Set lookup decides it.

That set is derived in the graph walk the anchor index already performs, so it
costs no extra pass.

  large_ms            7.79-8.37  ->  4.90-5.02   (1.61x faster)
  widening_overhead   2.50-2.82  ->  1.45-1.50

The resolved target-set fingerprint is byte-identical across both, which is
the point: this is a cost change, not a behaviour change.

Adds bench/callable-value-flow/ (fingerprint + scaling + widening-overhead
gates) and wires it into ci-tests.yml beside the other build-free benches. The
overhead budget of 1.9 sits between the measured with-filter and without-filter
bands, so it cannot be met if the pre-filter is removed. Timings use the MIN of
15 warmed reps, not the median: the same build reported 1.65 idle and 2.03
under load, and a median-based gate would have to be loosened past the point of
detecting the regression it exists to catch.

`buildGraphTargetIndex` is exported for the bench; it is pure and not part of
the pass's public contract.

* test(scope-resolution): assert the declaration route does not double-emit (#2693)

Go, Python, C++ and TS/JS already resolved a closure-binding call through
their `@declaration.function` capture. The widened `buildGraphTargetIndex`
gate gives the same call a SECOND possible route, so each must still produce
exactly one edge.

`tryEmitEdge` dedups by key, but a collapsed key and a site-anchored key are
DIFFERENT keys — a real double-emit would show up as two ids for one call
site, not be silently collapsed. Asserting on edge ids rather than target ids
is what makes that visible.

* fix(scope-resolution): join value bindings to their callable node by POSITION (#2693)

Review found the first cut of this series minted FALSE CALLS edges. Admitting a
value binding whose *resolved* graph node is callable let `resolveDefGraphId`
fall through to its label-agnostic, first-write-wins
`simpleKey(filePath, simpleName)` and bind the name to ANY same-named callable
in the file.

The safety argument in the previous commit — "a genuine constant keeps its own
Const/Property node, so the qualified key hits first" — silently assumed
`def.type === node.label`. It does not hold:

  - TypeScript declares `const` as `Variable` but emits a `Const` NODE, so the
    qualified key misses even though the value node exists;
  - Rust `let` bindings get no graph node at all, so the fallback is the only
    route.

Reproduced, all previously emitting a fabricated caller:

  const save = (x: number) => x * 2;   // next to an unrelated Svc.save
      -> Method:svc.ts:Svc.save#1      // Svc never instantiated
  const handler = other;               // shadowing a top-level handler
      -> Function:app.ts:handler       // unreachable from here
  let handler = cb;                    // Rust
      -> Function:main.rs:handler

Worse in Dart, where the same collision INVERTED the feature: the only edge went
to the class method and the closure's own node got none. The result was also
declaration-order dependent — two files differing only in declaration order got
different CALLS sets — and it propagated through argument-to-formal binding into
functions whose source never mentions the name.

A closure binding IS its callable node: same file, same line, same name. An
aliasing local is not. So the join is positional now — a file/line/name index
built in the graph walk `byAnchor` already performs — and value bindings never
run the key chain at all. That is both correct and cheaper:

  large_ms            4.90-5.02  ->  4.37-4.63
  widening_overhead   1.45-1.50  ->  1.43-1.58   (name-match design: 2.50-2.82)

with a byte-identical target-set fingerprint on the bench corpus.

Also from review:

  - `Static` dropped from VALUE_BINDING_DEF_TYPES: `normalizeNodeLabel` has no
    `static` case, so no def can carry that type — it was an entry no fixture
    could ever exercise. The remaining set now documents why it deliberately
    does NOT reuse `isOwnableValueLabel`, which is contracted to a different
    consumer.
  - Dart `final`/`const` top-level closures (static_final_declaration_list) and
    every declarator after the first in a multi-name local now resolve; both
    parse into shapes the earlier rules never reached.
  - The bench source carried a literal NUL byte, so git recorded it as BINARY
    and the only artifact pinning the target set was unreviewable in the PR
    diff. It is written as an escape now. Its corpus also modelled `startLine`
    as 1-based where graph nodes are 0-based, which would have stopped it
    exercising the value-binding path at all.
  - `call-summary-schema-version.test.ts` asserted `passesReuseGate(15)` is
    true; the 15 to 16 bump made that false and the test RED. It now pins 16 as
    current and 15 as rejected, matching the pattern every prior bump followed.
  - The v23 parse-cache comment is at the top of the list, not mid-list.

Tests: the five collision cases above are new regression tests, each confirmed
failing against the previous commit. Also added Kotlin class-body closures (the
only case exercising the Method arm), Dart top-level `final`, Dart multi-name
locals, and a warm-parse-cache replay for Kotlin and Dart — the #2693 captures
are replayed verbatim, so a serialization change would surface only on a SECOND
analyze and every other test here runs cold. The previous negative tests were
vacuous: they paired names that did not collide (`maxSize` vs `size`), so the
pre-filter rejected them before the guard they were named after could run.

* docs(storage): fix the schema-version changelog blocks (#2693)

Two problems, one mine and one not.

MINE: the `INCREMENTAL_SCHEMA_VERSION` block is ASCENDING (v2 … v15), and I
inserted v16 above v15 rather than at the end — I had just moved the parse-cache
entry to the top of ITS block, which is descending, and applied the same habit
to a list ordered the other way. Moved to the end; both blocks are now
internally consistent.

NOT MINE: the parse-cache block carries TWO v21 entries, with v20 wedged between
them. Tracing it: #2632 (Spring DI facts) bumped 20 -> 21 and merged first;
#2653 (Java JLS local-class identities) had branched at 20, also bumped to 21,
and merged second — so it shipped with NO invalidation of its own. An index
already stamped 21 by the first change was treated as current by the second and
kept serving stale local-class identities from the warm cache.

Numbers left alone: both genuinely shipped as 21, and renumbering them now would
misstate what users' indexes actually contain. Instead the entry says so
explicitly, and points at the process fix — re-check the constant against
origin/main immediately before merging, not just when the branch is cut. The
identical collision hit INCREMENTAL_SCHEMA_VERSION in #2653/#2654, so this is a
recurring failure mode of concurrent PRs, not a one-off typo.

Comment-only; no constant changes value.

* feat(scope-resolution): resolve closure bindings in Ruby, Java, C#, PHP and JS/TS var (#2693)

Ruby, Java, C# and PHP already emitted correct callable-flow seeds and invokes.
What they lacked was the #2687 piece — a CALLABLE graph node at the binding,
which is what buildGraphTargetIndex joins to by position. PHP additionally had
no scope declaration for the bound name, so the flow pass had nothing to attach
its seed to.

  ruby    handler = ->(x) { x }        handler.call(1)   -> Function:a.rb:handler
  java    Function<..> handler = x->x  handler.apply(1)  -> Function:A.java:A.handler
  csharp  Func<int,int> handler = ...  handler(1)        -> Function:A.cs:A.handler
  php     $handler = fn($x) => $x      $handler(1)       -> Function:a.php:handler

Ruby and Java invoke through the callable-object protocol; C# and PHP call the
binding directly. Locals work in all four, and a binding whose name collides
with a same-named method resolves to the CLOSURE, not the method.

Two things the sweep caught:

JAVA TWIN. Anchoring the rule on the inner variable_declarator produced BOTH a
Function and a Property node — the exact double-indexing #2687 removed. The
parse-worker dedup keys on (definition node, name), and Java's value rule
anchors on field_declaration, so the keys never matched. Re-anchored on
field_declaration / local_variable_declaration.

JS/TS `var`. `var f = (x) => x` kept a Variable label while const/let got
Function, because `var` is a different grammar node (variable_declaration vs
lexical_declaration) that no closure rule covered. A call through the binding
still resolved via the declaration route, so the CALLS edge pointed at a
NON-callable node. Now consistent across const/let/var.

That last one flipped an existing assertion in const-function-twin.test.ts,
which expected `Variable` for a var-bound function-expression. Its comment
explained why — "var has no matching @definition.function pattern, so nothing
claims the name" — i.e. it documented the gap rather than defending it. The
property it was really protecting (an UNCLAIMED value node survives) now has
its own case with a non-function initializer, and the var-closure case asserts
the collapse to one node, which is also the twin guard for the new rule.

Known limits, both pre-existing and both failing safe:

  - A PHP local closure whose name collides with a top-level function gets no
    edge: both want id Function:<file>:<name>, so the closure never gets its own
    node. This is the file-scoped node-identity convention — TypeScript, Python
    and Dart collapse identically at base.
  - TS/JS class-field arrows stay Property (Kotlin's equivalent emits Method).
    They already resolve; changing the label risks the HAS_PROPERTY ownership
    regression #2687 hit once.

The invalidation constants already bumped in this PR (INCREMENTAL_SCHEMA_VERSION
16, SCHEMA_BUMP 23) cover these additional languages; their notes now say so.

Tests: one case per newly-resolving language plus the PHP anonymous-function
form and the JS var form, in closure-binding-labels.test.ts. The file now spins
a worker pool per test across a dozen languages, so its timeout is raised
file-wide — a case that takes ~7s alone was exceeding the 30s default under
that contention.

* fix(ingestion): class-field closures are callable members in TS/JS (#2693)

A CALLS edge must target a callable node. `class A { handler = (x) => x }` emitted
a Property, so calling it produced `CALLS -> Property:A.ts:A.handler` — an edge
pointing at something the graph says is not callable. Same defect class as the
JS/TS `var` binding fixed in the previous commit, and the last place a closure
binding still carried a value label.

Kotlin already models its class-body closure as Method + HAS_METHOD; TS/JS now
match, so all three agree:

  class-field closure   -> Method   + HAS_METHOD    (CALLS target is callable)
  plain class field     -> Property + HAS_PROPERTY  (unchanged, no CALLS)

Anchored on public_field_definition / field_definition — the same nodes the
property rules use — so the parse-worker dedup collapses the pair rather than
leaving a Method/Property twin, the failure the Java rule hit in the previous
commit.

ON MATCHING THE COMPILERS. This deliberately diverges from tsc and SCIP. The
TypeScript compiler classes `handler = () => {}` as a PropertyDeclaration
("a property declaration independently from what it's assigned to"), and SCIP
gives it a `.` term descriptor, the same suffix as any field — both call it a
property, and Kotlin's compiler likewise treats `val f = { }` as a property with
a function type. The divergence is intentional: GitNexus's Function/Method label
does not mean "tsc SymbolFlags", it means "this node can be the target of a
CALLS edge", which is the convention #2687 set for closure bindings in every
language. Modelling it the compiler's way would mean either dropping call
resolution for these members or emitting a separate node for the lambda and
flowing the property to it — the two-node shape #2687 removed. Recorded here so
the next reader does not "fix" it back.

Tests: TS and JS class-field arrows resolve to their Method node, plus a guard
that a NON-closure class field stays a Property — the closure rule must key on
the initializer, not on the field syntax.

* fix(php): keep the $ sigil on closure-binding nodes so locals stop colliding (#2693)

A PHP local closure whose name matched a file-level function got NO edge at all:

    function save($x) { return $x; }
    function run() {
      $save = fn($x) => $x * 2;
      return $save(1);              // no CALLS edge
    }

Both minted the id Function:<file>:save, so the closure's node was swallowed by
the function's and the positional join found nothing at the binding's line.

The fix is PHP's own semantics rather than a change to node identity across the
graph. PHP holds variables and functions in SEPARATE namespaces — $save and
save() cannot collide in the language — and the sigil is what separates them.
Dropping it was the bug. The node rule now captures the whole variable_name, so
the closure is Function:<file>:$save and the function stays Function:<file>:save.
languages/php/query.ts already keeps the sigil on property declarations for the
same reason, so this makes the two consistent.

The positional join normalises a leading $/@ on both sides, matching what the
scope layer and the callable-flow synthesizer already do, so the binding still
matches its own declaration while its NODE stays distinct.

    local closure + same-named function -> Function:c.php:$save   (the closure)
    calling the real function           -> Function:f.php:save    (unchanged)
    plain $max = 10                     -> no node, no edge       (unchanged)

WHAT THIS DOES NOT FIX. The general problem is wider than PHP: GitNexus node ids
are file-scoped, so a function-local symbol and a file-level one with the same
name collapse in TypeScript, Python and Dart too, and Java/C# only escape by
qualifying on the enclosing CLASS (so two same-named locals in different methods
still collide). SCIP solves it with a separate `local <id>` keyspace that is
document-scoped and never globally addressable. That is issue #2699 — it changes
persisted ids for every function-local symbol and needs its own invalidation, so
it is not bundled here. PHP is fixed on its own merits: the sigil belongs in the
identity regardless of how locals are eventually scoped.

* test(scope-resolution): pin the closure-binding caller-attribution limit (#2693)

Review of this PR found the new callable nodes are call TARGETS but never call
SOURCES: a call made INSIDE a closure binding is attributed to the enclosing
scope, so `impact(handler, direction:"downstream")` reports nothing even though
the closure calls out. Consistent across Kotlin, Dart, Ruby and PHP; TS/JS free
bindings are the exception because their arrow carries a @scope.function whose
range matches.

Not fixed here — pinned, so the boundary is visible instead of surprising, and
so a change in EITHER direction fails a test.

The cause is precise: `pickCallerCallableDef` (graph-bridge/ids.ts) finds the
caller by walking CHILD scopes whose range contains the call site, gated on
`child.kind === 'Function'`. A closure literal is a BLOCK scope in these
languages (Kotlin deliberately, #1757 smart casts), AND the binding's def is
owned by the enclosing scope rather than by the closure's scope — so neither
half of the link exists. Fixing it needs "callable boundary" decoupled from
scope `kind` plus an association between the closure scope and its binding.
That is a change to the caller anchor used by every call in the repo, which is
not something to land at the tail of this PR.

Also adds a unit suite for `buildGraphTargetIndex` itself, covering what the
integration tier cannot isolate: a binding is admitted only on POSITIONAL
evidence, a name-only match is rejected, a non-callable node at that position is
rejected, an ambiguous position claimed by two callables is rejected, and the
PHP dollar sigil normalises across the join while still not matching a
same-named function on another line. That last one closes the review's LOW —
the node/declaration name asymmetry now has an executable contract rather than
resting on a comment.

* docs(test): correct the per-language cause of the attribution limit (#2693)

The comment on the pinned attribution tests claimed "a closure literal is a
BLOCK scope in these languages". That is true for Kotlin (lambda_literal
@scope.block, #1757) and Ruby (do_block/block @scope.block) and FALSE for PHP:
anonymous_function and arrow_function are already @scope.function
(php/query.ts:61-62). Dart is a third case again — it has no scope over a
closure literal at all.

So the four languages fail at three different points, not one:

  Kotlin, Ruby  fail the `child.kind === 'Function'` gate
  PHP           passes that gate; its closure scope owns no callable def,
                because the binding's def belongs to the enclosing scope
  Dart          has no child scope for the walk to consider

Worth correcting carefully rather than tidying: a follow-up plan re-stated this
comment instead of re-deriving it, and inherited the misdiagnosis — it proposed
"relax the kind gate" as required for all four, which is a no-op for PHP and
unreachable for Dart. A review caught it. The comment now states each language's
actual blocker and says why the distinction matters.

Comment-only; the three pinned tests are unchanged and still pass.

* fix(scope-resolution): an ordinary JS/TS `function` binds its own `this` (#2701)

`this.m()` inside a nested `function` resolved to the lexically enclosing
class, so it emitted a CALLS edge that does not exist at runtime — including
the exact `forEach(function () { this.m(); })` shape arrow functions were
introduced to avoid:

    class D {
      m() {}
      build() { const h = function () { this.m(); }; return h; }
    }
    // CALLS: Function:D.ts:D.h -> Method:D.ts:D.m#0      FALSE

ECMA-262 gives an arrow `[[ThisMode]] = lexical`: it has no `this` binding in
its environment record, so the lookup passes through to the enclosing
environment. Every other function form binds `this` at call time. `tsc` draws
the same line by resolving `this` through `getThisContainer` with
`includeArrowFunctions = false`. That one rule is the whole fix.

Languages declare it; shared code never learns a language. The query files —
the one place that already names grammar nodes — tag every non-arrow function
form with `@receiver-owner.this`, which becomes `Scope.ownsReceivers`. A
receiver walk that reaches such a scope without finding the name stops there
instead of borrowing an enclosing scope's binding. Every other language leaves
the field unset and is bit-for-bit unchanged; a Kotlin lambda, which DOES
capture the enclosing `this`, still resolves (pinned as a test).

THREE GATES, ALL LOAD-BEARING. The false edge survived each one alone, which
is why the tests assert on the emitted edge rather than any single walk:

  1. `Scope.ownsReceivers` stops BOTH receiver-type walks — `findReceiver
     TypeBinding` here and its twin `lookupReceiverType` in gitnexus-shared's
     `lookup-core`, which was resolving the receiver independently.
  2. `LanguageTypeConfig.thisBoundaryNodeTypes` stops the type-env AST walk
     that infers a receiver's type during capture.
  3. `isReceiverOwnedButUnbound` makes `receiver-bound-calls` SUPPRESS the
     site. Without it the member still resolved by NAME through `lookupCore`'s
     lexical chain — the class-body scope binds `m` two scopes up — merely at
     lower confidence. An owned-but-unbound receiver is a definitive negative,
     not a miss, so it must not reach a receiver-blind fallback.

Also fixed: `function*(){}` as an expression was not a `@scope.function` at
all, so `this` inside one read as the enclosing method's.

WHAT THIS GIVES UP. The fix REMOVES edges, and some were correct:
`.bind(this)`, `.call(this)` and `forEach(fn, thisArg)` do make `this` the
instance at runtime. Their correctness is fixed at the CALL SITE, which no
scope-level rule can see, so the choice is between losing them and keeping
every detached-callback false positive. All three are pinned as tests
asserting the empty result, so changing the trade later is deliberate.
`this` in a static method also stops resolving to the INSTANCE member — that
edge was wrong in the other direction.

INVALIDATION. Both constants move, and the parse-cache one is not optional:
`ownsReceivers` lives on the cached `Scope`, and a warm cache replays scopes
without it — verified by probe that `--force` alone does NOT re-derive it, so
the fix silently did nothing until SCHEMA_BUMP moved. INCREMENTAL_SCHEMA_
VERSION 16 -> 17 (the incremental write set covers only changed files, so
unchanged TS/JS files would keep their fabricated `this` edges);
SCHEMA_BUMP 23 -> 24.

Verified against a built index, not by reading: all three false edges from the
issue gone, every correct edge kept, same result in JavaScript through its
separate grammar. 64 tests green across the new suite plus the closure-binding
and schema-version suites. The full suite's 36 failures are pre-existing
load-flakes — confirmed by A/B: `skip-git-cli` fails FOUR tests on a clean
HEAD versus three with this change, and `pipeline-pdg-streaming` passes in
isolation either way.

Refs #2701

* fix(ingestion): give function-local callables their own identity (#2699)

Graph node ids were file-scoped, so a local callable and a same-named
file-level one collapsed onto ONE node. That is a wrong answer, not a missing
one — the local call was attributed to the file-level symbol:

    export function save(x) { return x; }
    export function run()   { const save = x => x * 2; return save(1); }
    export function other() { const save = x => x * 3; return save(2); }

    // ONE node Function:a.ts:save, and BOTH run and other pointed at it, so
    // `impact` on the top-level save reported two callers that never call it.

A local's identity is now its enclosing-callable chain plus its own position —
`run.save@2:2`. The chain is for humans reading `impact`; the position is what
makes it correct. Names alone cannot express what ECMAScript actually
specifies, and the gap is the language's, not the grammar's: an environment
record is created per function AND per block, so an anonymous function has no
name to contribute and sibling blocks hold distinct bindings under the same
name. One positional rule settles both, with no conditionals and no
"disambiguate only when it looks ambiguous" heuristic — the ambiguity-flag
class of bug that bit #2514. SCIP reaches the same place with its
document-scoped `local <id>` keyspace.

Top-level functions and class methods are NOT locals and keep their ids
byte-for-byte. That is the bound on the churn: this touches only symbols that
are unreachable from outside their own document anyway.

RESOLUTION JOINS BY POSITION, NOT BY NAME. `resolveDefGraphId` matches a def
to its node on (file, label, line, simple name). A def and its node are the
same construct, so this needs no scope chain at all — which is the point:
re-deriving the chain in the resolver would be a second implementation that
could silently disagree with the first. A genuine tie (two callables on one
line) stores an AMBIGUOUS_POSITION tombstone and falls through to the existing
name keys rather than picking by source order. Without this the node ids were
already correct and calls STILL resolved to the file-level symbol — the fix is
only half a fix without it.

JS/TS GAIN BLOCK SCOPES. They emitted no `@scope.block` at all, so the
resolver could not tell two `const pick` in sibling branches apart. Giving
them distinct ids made that visible as DUPLICATE edges — each call resolving
to BOTH — which is worse than the collapse it replaced. `(statement_block)
@scope.block` supplies the missing environment record. The other half of the
ECMAScript rule was already implemented and waiting: `tsBindingScopeFor`
hoists `var` past blocks to the enclosing Function/Module while `let`/`const`
bind innermost, and its docblock already claimed "the innermost default covers
these" for block scopes that did not exist. All 82 scope-resolution test files
pass with blocks on.

Verified by probe, per case: two locals in different functions, a local inside
an ANONYMOUS function (`outer.fn@1:9.save@2:4`), sibling blocks resolving to
their own binding, `var` still hoisting out of its block, a nested named
`function` vs a file-level one, PHP composing with the `$` sigil from #2693,
and Python. Top-level/method ids unchanged, asserted directly.

Every assertion is on the EDGE, not on node existence. Ids are built twice and
independently — definition phase and caller attribution — and a one-character
disagreement makes the caller attach to a node that does not exist and the
edge vanish, with nothing thrown and no test failing. An edge assertion can
only pass if both phases agree.

INVALIDATION. INCREMENTAL_SCHEMA_VERSION 17 -> 18 and SCHEMA_BUMP 24 -> 25:
persisted node ids change for every function-local callable, and the cached
scope tree lacks block scopes. A top-up would leave unchanged files on the old
ids while changed files emit the new ones, splitting each symbol in two.

Bench fingerprint unchanged and both timing budgets pass. The one full-suite
failure (incremental-orchestration) passes in isolation — its log shows stale
init locks and WAL reclaim, i.e. LadybugDB contention under the parallel run.

Refs #2699

* perf(ingestion): emit block scopes only where they bind something (#2699)

Block scopes make `let`/`const` in sibling blocks distinct bindings, which is
what stopped a call in one branch resolving to both. Emitted naively — one
scope per `statement_block` — they also cost ~10% of analyze wall time, because
every scope-chain walk in every function then steps through levels that bind
nothing.

Two emit-side filters keep the semantics and drop the waste:

  1. A block that IS a function body duplicates the enclosing Function scope.
     Nothing can be declared between a function and its own body, so a binding
     in either resolves identically — the inner scope is pure depth.
  2. A block that declares no `let`/`const`/`class`/`function` binds nothing,
     so it is transparent: a lookup finds nothing in it and walks to the
     parent. `var` is deliberately excluded from that list — it hoists past the
     block to the function, so a block containing only `var` still binds
     nothing.

MEASURED, on a 762-file / 228k-line TypeScript corpus (gitnexus/src), min of 6
warmed reps with the cold first rep discarded:

    block scopes emitted   19,389  ->  5,331     (-72%)
    total scopes           35,942  ->  21,884    (-39%)
    analyze wall time      +9.8%   ->  +1.6-2.5% vs pre-#2699
    peak RSS (whole tree)  2398MB  ->  2434MB    (+1.5%, inside run-to-run noise)

The filters themselves are free: scope emission over the same corpus measured
12.6s naive vs 12.5s filtered.

Wall-clock on a shared runner has a ±10% spread run to run, which is wider than
the effect being optimised, so the durable gate added here counts scopes
instead. `bench/scope-emission/measure.mjs --check` asserts an EXACT scope set
over a synthetic corpus that mixes the shapes the filters discriminate between
— function/method/arrow bodies, non-declaring if/else/for/while/try, blocks
that declare `const`, and a `var`-only block. Baseline is 2 block scopes per
module: only the two `if`/`else` branches that declare `const chosen`. If the
filters regress that number jumps immediately, in a way wall-clock CI could
never resolve from noise. Wired into the existing benchmarks job.

Behaviour is unchanged: 86 scope-resolution and identity test files, 1371
tests, all green — including the sibling-block case this could plausibly have
broken — and the callable-value-flow fingerprint is untouched.

Refs #2699

* test(bench): re-baseline the TS/JS scope-capture fingerprints for #2701

`bench/scope-capture` fingerprints the full capture set per language, and
#2701 added a `@receiver-owner.this` marker to every non-arrow function form
so a scope that BINDS its own `this` can terminate the receiver walk. That is
a capture-set change, so the TypeScript and JavaScript fingerprints moved and
the benchmarks job has been failing since that commit — I pushed it without
checking CI.

A fingerprint is a correctness gate, so this does not simply adopt the new
value. Verified first by diffing the capture-name HISTOGRAM over the same
fixture corpus against 1d308817 (the commit before #2701), which says what a
fingerprint cannot: WHICH names moved.

    typescript   @receiver-owner.this   0 -> 143
    javascript   @receiver-owner.this   0 -> 32

Nothing else. Every other capture count is byte-identical, so no existing
capture shifted and the drift is entirely the intended marker. Both languages'
scaling ratios stay well inside their 1.5 budgets (0.976 / 1.025).

Note `@scope.block` does not appear in the delta: the #2699 filters suppress a
block that is a function body or that declares no binding, and no fixture in
this corpus has a block that binds. Block-scope emission is guarded separately
by `bench/scope-emission`, whose synthetic corpus exercises exactly those
shapes.

Refs #2701

* fix(ingestion): stop the callable-prefix walk at class bodies, not only declarations (#2699)

An anonymous class owns its members, but `CLASS_CONTAINER_TYPES` lists only class
DECLARATION nodes — and a Java anonymous class has none. It is

    object_creation_expression > class_body > method_declaration

so `enclosingCallablePrefix` sailed straight through the anonymous body, reached the
enclosing method, and re-keyed the member as a function-local of that method:

    Method:src/Worker.java:Worker$1.run#0
    -> Method:src/Worker.java:Worker.makeHandler.run@7:12#0

That destroys the javac-compatible JLS identity #2550/#2555/#2562 exist to provide, and
broke four existing Java tests that this PR never touched — anonymous-class instance
identity, local-type identity, and enum-constant-body chaining.

The design was right; the boundary was blind. `CALLABLE_PREFIX_BOUNDARY_TYPES` adds the
body and anonymous-construction forms (`class_body`, `interface_body`,
`annotation_type_body`, `enum_body`, `enum_body_declarations`, `enum_constant`,
`object_creation_expression`, `object_literal`,
`anonymous_object_creation_expression`). Over-inclusion is the SAFE direction here: an
extra boundary only suppresses the nesting prefix, falling back to the pre-#2699 class
qualification.

This also falsifies the claim in the #2699 commit that "top-level functions and class
methods keep their ids byte-for-byte" — an anonymous-class method IS a class method, and
its id did change. The claim was true only for the shapes that were tested.

Also removes the dead `NO_QUALIFIED_NAME` constant, which contained a literal NUL byte.
That byte made `file(1)` report the source as `data` and made plain `grep` return zero
matches for ANY pattern in the whole 2,928-line file — which is why several greps during
development came back mysteriously empty. Two other files carry NULs; they are
pre-existing and out of scope here.

INVALIDATION. INCREMENTAL_SCHEMA_VERSION 18 -> 19 and SCHEMA_BUMP 25 -> 26. This is not
defensive: an index stamped v18 holds the WRONG Java ids, and without the bump it passes
the `=== INCREMENTAL_SCHEMA_VERSION` reuse gate and keeps them on every unchanged file.

Found by the PR #2695 tri-review (review 4782134453) — independently by a Claude
adversarial AST probe, by Codex's swarm, and by CI (`tests / ubuntu / coverage 2/3`).
Verified: `resolvers/java.test.ts` 247/247 (was 243/247), plus this-boundary,
function-local-identity and the schema-version suites.

Refs #2699

* fix(scope-resolution): fail closed when a function-local shadows a same-named callable (#2699)

The #2699 positional join failed OPEN. On a position miss `resolveDefGraphId` fell
through to the label-agnostic, first-write-wins `simpleKey(filePath, simpleName)`, which
aliases a def onto whichever same-named callable was registered first — the exact
fabricated-caller mechanism this PR's own #2693 work already shipped once as a P0.

It misses because the two id phases anchor on different nodes BY DESIGN:
`tree-sitter-queries.ts` anchors the graph node on the outer `lexical_declaration`, while
`languages/typescript/query.ts` anchors the scope def on the inner `arrow_function` so
`anchor.range` lines up with `@scope.function` for auto-hoist. Split the declaration
across lines and those land on different LINES:

    export function run()   { const pick =
        (x) => x * 2; return pick(1); }
    export function other() { const pick =
        (x) => x * 3; return pick(2); }

    before:  run   -> run.pick@1:2      correct
             other -> other.pick@6:2    correct
             other -> run.pick@1:2      FABRICATED — other() never calls run's pick

Every fixture in function-local-identity.test.ts kept the declaration and its initializer
on ONE line, where the anchors coincide. That is why the suite stayed green while the bug
shipped, and the new test deliberately splits them.

WHY NOT A BLANKET FAIL-CLOSED. A position miss is not always a collision: it also happens
where the anchors legitimately differ, e.g. a Vue SFC, whose graph nodes carry
`+ lineOffset` while scope extraction does not. Failing closed on every miss would delete
correct edges there. So the guard is keyed on evidence that the collision is REAL —
`localNameKey` records that a function-local of this simple name exists in the file
(local-identity nodes are recognisable by the `@<row>:<col>` on their last name segment).
Only then is a miss treated as ambiguity. Files with no such local keep their previous
fallback behaviour byte-for-byte.

A missing edge is the correct failure direction here: `impact` can recover from an absent
caller, but a fabricated one silently corrupts the answer.

WHY NOT UNIFY THE ANCHORS. Considered and rejected: the split is deliberate and
load-bearing for auto-hoist across every language (the `rangesEqual(anchor.range,
innermost.range)` rule), so unifying it would fight that discipline far outside this fix.

The regression test was verified to DISCRIMINATE: with the guard disabled it fails on
exactly the fabricated edge (`+ "Function:m.ts:other -> Function:m.ts:run.pick@1:2"`).

impact(resolveDefGraphId, upstream) is CRITICAL — 62 impacted, 23 direct, 6 flows — which
is precisely why the guard is gated rather than broad. detect_changes: HIGH, 8 affected
processes, all in EmitReceiverBoundCalls / EmitRubyMixinEdges. Verified: 85 test files /
1364 tests green, including every scope-resolution unit.

Found by the PR #2695 tri-review (review 4782134453): raised by Codex's adversarial leg,
mechanism source-confirmed during synthesis, then reproduced end-to-end.

Refs #2699

* fix(typescript): stop the enclosing-type walk at nodes that rebind `this` (#2701)

`findEnclosingType` walked `node.parent` to the top of the file with no boundary, so it
happily synthesized a `this` binding from a type that does not own the member:

    class A { outer() { const o = { inner() { return this.x; } }; return o; } }

`this` inside `o.inner` is `o`, never `A` — but the walk reached `A` and bound to it, so
every `this.…` in such a method resolved against the wrong type. Only the module-level
object literal escaped, because there was no enclosing class to reach. Applies to
JavaScript too: `languages/javascript/captures.ts` calls the same function.

Boundary set: object literals and the function forms that rebind `this` at call time.
Arrows are deliberately absent — they inherit `this` lexically, which is what makes a
class-field arrow `m = () => this.x` resolve.

WHY THE MARKER WAS NOT ALSO REMOVED FROM METHOD FORMS.

The review argued `@receiver-owner.this` over-suppresses: `synthesizeTsReceiverBinding`
returns null for static members, object-literal methods and anonymous class expressions,
so those scopes are "owned but unbound" and get suppressed, losing edges the base
resolved. Removing the marker from the method forms was tried and MEASURED, and the
result does not support shipping it:

    marker removed, probe of all five shapes:
      static -> static            RESTORED (true)
      object literal (module)     RESTORED (true)
      anonymous class expression  RESTORED (true)
      static -> INSTANCE          FALSE EDGE returned
      object literal in a class   FALSE EDGE (Nested.outer.inner -> Nested.x)

The last one is the point: this fix stops the false *synthesis*, but removing the marker
re-enables receiver-blind *name* resolution in `lookupCore`'s lexical chain, which
recreates the same wrong edge by another route. The restored edges and the false ones
come from the SAME mechanism — a name walk — so they cannot be separated by toggling the
marker. The real trade is 2 genuinely-new true edges for 2 false ones, not the 3-for-1
the plan assumed.

Corpus evidence (762 real TypeScript files, edge SETS not counts, cold cache both arms):

    baseline vs marker-removed:  net 0, REMOVED 0, ADDED 0

Neither the gains nor the losses occur in production code. Given a 1:1 true/false ratio
on synthetic shapes and zero effect on real ones, the marker stays: for a graph feeding
`impact`, a fabricated caller is worse than an absent one — the same principle applied in
the fail-closed positional join. The three shapes remain UNRESOLVED rather than wrongly
resolved; resolving them properly needs a typed binding for object literals, anonymous
classes and static contexts, which is a feature, not this fix.

Measured with an edge-SET diff harness, after both ce-doc-review passes established that
an edge COUNT cannot decide this (it conflates edges gained with edges lost, so a
near-zero net reads as "no regression"). The harness also had to wipe the index each arm
— a warm parse cache initially reported an unchanged edge set across a real behavioural
change, the same trap documented in the v24 SCHEMA_BUMP note.

detect_changes: low risk, 3 symbols, no affected processes. 85 files / 1365 tests green.

Refs #2701

* docs(test): correct the false "three load-bearing gates" claim (#2701)

The header of `this-boundary.test.ts` asserted that all three gates were
independently load-bearing because "the false edge survived removing any one of
them alone". That was true DURING development, measured incrementally, and was
carried into the shipped comment without being re-tested against the finished
code. It is false: gate 3 (`isReceiverOwnedButUnbound` in `receiver-bound-calls`)
runs FIRST and marks the site in `handledSites`, which `emitReferencesViaLookup`
then skips — so for an explicit `this` receiver it subsumes gate 1. Removing
gate 1's `ownsReceivers` check in `gitnexus-shared/.../lookup-core.ts` leaves all
10 tests in the file passing; verified by experiment.

The gate is RETAINED, and the review's recommendation to delete it as "dead" is
rejected on evidence. `receiver-bound-calls` only suppresses EXPLICIT receivers
(`if (site.explicitReceiver === undefined) continue;`), whereas `lookup-core`'s
gate is also reached for IMPLICIT ones through `IMPLICIT_RECEIVERS` in
`resolveReceiverOwner` — a bare `m()` inside a nested `function` inside a method
goes down that path. The experiment shows the gate is UNTESTED, not unreachable;
those are different claims and only the first is supported. Deleting it on the
strength of a green test run would have removed live code, which is the same
reasoning error the corrected comment is about.

This is a documentation-only change: no behaviour, no test expectations. The
correction is recorded in place rather than silently rewritten, because the way
the claim came to be wrong — measured on an intermediate tree, then asserted
about the final one — is the reusable lesson.

Refs #2701

* test(scope-resolution): pin the block-scope ACCESSES delta as false-edge removal (#2699)

The tri-review flagged that enabling `(statement_block) @scope.block` for
JS/TS drops 114 `ACCESSES -> Const` edges corpus-wide with `added: 0`,
undocumented and untested. That was recorded as a suspected regression.

It is not one. All 274 emitting reference sites behind those 114 edges were
classified by re-reading the source at the site: 269 are member reads, the 5
others are classifier artifacts (the name recurs earlier on the line, as in
`a.b.declLine` for `b`) and are member reads too. No edge was
bare-identifier-only. Every dropped edge was a property read
(`options.baseUrl`) mis-resolving to an unrelated function-local `const` of
the same name in the same file.

The cause is not block-specific: `lookupCore` Step 1 walks the lexical chain
for every lookup, including explicit-receiver property reads. Block scopes do
not fix that, they narrow it, by moving the local off the chain of any
reference outside its block. A local declared directly in the function body
still hijacks the read; that is pre-existing and left alone here.

Two tests. The first discriminates: it fails with the block capture removed
(the false edge reappears) and passes with it. The second is a companion
invariant, identical in both arms, so that "the edge went away" cannot be
satisfied by a change that dropped Block-kind bindings outright.

Fixture notes, both of which defeated earlier attempts at this edge class:
`pruneLocalSymbols` deletes ~94% of function-local value symbols, so the
`const` under test must be kept via `keepLocalValueSymbols`; and the member
read must sit outside the block, since inside it the block is on the
reference's own chain and the false edge appears in both arms.

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

* docs(ingestion): correct the SCIP citation on the function-local id (#2699)

The comment justified the positional, name-bearing local id (`fn@12:9`) as
"same reasoning as SCIP's document-scoped `local <id>` keyspace". SCIP is the
wrong citation for this key shape: its `local <id>` is a per-document counter,
and the spec states that locals do not encode the name.

SCIP remains prior art for the document-scoped keyspace itself, which is the
part the argument actually leans on, so the reference is corrected rather than
dropped. clang's USR for a function-local (`name@offset`) and Kythe's C++
indexer are the accurate citations for a positional, name-bearing key.

Comment only, no behavior change.

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

* fix(typescript,javascript): sync the function node-type lists, and test it (#2701)

Four hand-maintained lists answer "which node types are function-like":

  1. `query.ts` — the `@scope.function` / `@receiver-owner.this` patterns
  2. `captures.ts` — `FUNCTION_NODE_TYPES` (callable-flow synthesis + the
     body-block filter)
  3. `receiver-binding.ts` — `THIS_REBINDING_BOUNDARY_TYPES`
  4. `type-extractors/typescript.ts` — `THIS_BOUNDARY_NODE_TYPES`, whose
     docstring already claimed it was "kept in sync with `@receiver-owner.this`"
     with nothing enforcing it

`generator_function` (the EXPRESSION form, `const g = function* () {}`) was
added to both queries for #2701 and is present in lists 3 and 4, but was
missing from both `FUNCTION_NODE_TYPES`. Added.

That gap changes no graph output today, and the commit does not claim
otherwise. Measured on `const g = function* (x) { yield x; }; g(1)`: node and
edge sets are byte-identical with and without the entry. The `this` boundary
was already correct via the query marker — `this-boundary.test.ts` has a
passing generator case. A generator-expression binding still emits a `Const`
node rather than a `Function` one, so its call resolves to nothing either way;
that label comes from the definition rules, and closing it is a separate change
NOT made here.

So the entry is list consistency and the test is the real deliverable. It
asserts lists 1 and 2 EQUAL, and lists 3 and 4 as subsets of the query markers
with an explicit allowlist — the method forms bind their own `this` but the
class is their `this`-owner, so neither walk may stop there. Verified
discriminating: removing the `generator_function` entry fails both equality
assertions.

The lists are exported for the test; no other production surface changes.

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

* test(bench): gate scope emission per language, not TypeScript-only (#2699)

The scope-emission gate ran the TypeScript emitter only, so a JavaScript-only
regression shipped green. The two filters it guards are implemented twice —
`FUNCTION_BODY_OWNER_TYPES` in `typescript/captures.ts` and
`JS_FUNCTION_BODY_OWNER_TYPES` in `javascript/captures.ts`, each with its own
`blockDeclaresBinding` and `BLOCK_BINDING_CHILD_TYPES` — so covering one said
nothing about the other.

Adds a structurally parallel JavaScript corpus (the same shapes with the
TS-only syntax removed) and splits `baselines.json` per language. `--check`
now also fails when a baselined language is not measured, which is how a gate
goes quietly green.

Verified the new arm bites: disabling the JS body-block filter alone takes
JavaScript from 400 to 600 block scopes and fails `--check`, while TypeScript
stays green — the exact regression the old gate would have passed.

The two languages happen to agree exactly on this corpus (2 blocks per module,
2200 scopes). That is recorded as a measured result, not an invariant: each
language is still gated against its own baseline. TypeScript's numbers are
unchanged.

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

---------

Co-authored-by: Gergo Magyar <abhigyan1.patwari@gmail.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 07:52:18 +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 fix: index staleness — false-stale status after analyze (#2668) + inline staleness in query/context/impact/cypher tools (#2655) (#2683) 2026-07-25 07:21:44 +01: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 fix(scope-resolution): resolve calls through a closure-valued binding across languages (#2693) (#2695) 2026-07-27 07:52:18 +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
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 fix(ingestion): stop double-indexing const X = () => {} as Function + edgeless Const twin (#2687) (#2691) 2026-07-25 16:56:17 +01:00
gitnexus fix(scope-resolution): resolve calls through a closure-valued binding across languages (#2693) (#2695) 2026-07-27 07:52:18 +01:00
gitnexus-claude-plugin fix: index staleness — false-stale status after analyze (#2668) + inline staleness in query/context/impact/cypher tools (#2655) (#2683) 2026-07-25 07:21:44 +01:00
gitnexus-cursor-integration feat(ci): review agent runs as a coordinated reviewer swarm (#2572) 2026-07-20 07:31:40 +01:00
gitnexus-shared fix(scope-resolution): resolve calls through a closure-valued binding across languages (#2693) (#2695) 2026-07-27 07:52:18 +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 Merge pull request #2643 from abhigyanpatwari/dependabot/npm_and_yarn/gitnexus-web/lru-cache-11.5.2 2026-07-23 11:56:12 +05:30
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(ci): review agent runs as a coordinated reviewer swarm (#2572) 2026-07-20 07:31:40 +01:00
ARCHITECTURE.md fix(scope-resolution): resolve callable reference flows (#2437) (#2522) 2026-07-17 17:20:02 +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(ci): review agent runs as a coordinated reviewer swarm (#2572) 2026-07-20 07:31:40 +01: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 ci: move Node pins to the 22.18 floor 2026-07-21 10:09:34 +00: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(web): support GITNEXUS_BACKEND_URL env var for Docker deployments (#1286) 2026-05-25 11:21:11 +01:00
docker-server.test.mjs feat(web): support GITNEXUS_BACKEND_URL env var for Docker deployments (#1286) 2026-05-25 11:21:11 +01:00
Dockerfile.cli fix(docker): ship runtime-needed published assets (hooks/, skills/) into the image (#2130) (#2132) 2026-06-10 08:38:37 +01: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 perf(communities): fix the O(communities x N) copy in vendored Leiden, wire Icebug to its real API (#2337) (#2692) 2026-07-25 13:23:17 +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(ingestion): stop double-indexing const X = () => {} as Function + edgeless Const twin (#2687) (#2691) 2026-07-25 16:56:17 +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 Merge remote-tracking branch 'upstream/main' 2026-07-23 14:50:09 +08:00
RUNBOOK.md fix(storage): rename index metadata to gitnexus.json with dual-write compatibility (#2363) 2026-07-03 19:32:59 +01:00
SECURITY.md ci(security): add automated security and vulnerability scans (#1297) 2026-05-04 08:21:53 +01: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.

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