Code Graph Review

Language Server Protocol Index Format and Cross-Repo Navigation

SCIP replaces LSIF's graph model to simplify cross-repository code intelligence at scale.

Editor at Large · · 9 min read
Cover illustration for “Language Server Protocol Index Format and Cross-Repo Navigation”
Code Navigation in IDEs · September 23, 2026 · 9 min read · 1,996 words

The Language Server Protocol changed how editors get smart about code: instead of every editor building its own understanding of every language, a single language server does the analysis and speaks a common JSON-RPC protocol to whatever client wants to ask it questions. Autocomplete, go-to-definition, find-all-references, hover documentation: all of it flows through this one channel. LSIF is what happens when someone asks a harder question: what if you didn't want to run that language server at all, but still wanted its answers?

That's the subject of this piece, and the answer traces a path from a graph-shaped format called LSIF, through its eventual replacement, SCIP, and into a genuinely different way of thinking about how code intelligence should travel across repository boundaries.

LSIF: a snapshot of a language server's knowledge

LSIF stands for Language Server Index Format, pronounced "else if," and it exists to solve a specific problem: a language server's knowledge of a codebase is expensive to compute and lives only as long as the process runs. LSIF proposes freezing that knowledge into a file. The dump captures what the language server knows, so that later, a tool can answer editor-style requests (where's this defined, what hovers here, what references this symbol) without the language server being alive.

The specification deliberately avoids saying how that dump should be stored. It's a format, not a database engine, so a team can drop it in a flat file, load it into a graph database, or import it into whatever indexing layer they already run. Pinning a spec to one storage technology limits its adopters to whoever likes that technology.

Not everything a language server can do belongs in the dump, though. Code completion is a good example. Completion suggestions depend on the exact state of a document mid-edit, and that state stops being valid the moment a keystroke lands. So LSIF captures the durable stuff, definitions, references, hover text, and skips the requests whose answers expire on contact with a live editing session.

How LSIF encodes navigation data as a graph

LSIF's answer to "how do you represent code knowledge" is a graph, and it borrows its vocabulary directly from LSP itself. Edges in the graph carry the names of LSP request methods. Vertices represent the nouns: documents, ranges within those documents, and results, like a hover result or a set of reference locations.

Take a concrete case laid out in the spec: a document vertex with an id and a URI and a language ID, a hoverResult vertex holding the actual hover contents, and a textDocument/hover edge connecting a range in that document to the hover result. Emit enough of these vertices and edges as a sequence of JSON objects, one per line, and you have a working LSIF dump.

LSIF encodes ranges, not positions. A hover result for an identifier applies to every character in that identifier, not just one cursor position, so instead of repeating the same result for each character offset, the format defines a start and end (line and character) and attaches the result once to that whole span. It's a small choice, but it keeps dumps from bloating out on nothing but redundant repetition.

Then there's the contains edge, which ties ranges back to the document they live in. Because a single document contains many ranges, LSIF allows this to be expressed as a one-to-many relationship rather than forcing a repeated edge per range. That, again, is about compactness, and about making it feasible to batch-import a dump of any real size.

How monikers make LSIF dumps portable across repository boundaries

Graphs solve navigation within a single dump. They don't, by themselves, solve navigation between two dumps produced by two different projects, and that gap is where LSIF's real ambition becomes visible. Say project A imports a library, project B. Each has its own LSIF dump, built independently, with no shared vertex IDs and no awareness of each other. When a developer in project A asks to jump to the definition of something that lives in B, what actually resolves that request?

The answer, introduced formally in version 0.4.0 of the spec, is the moniker. A moniker is an optional identifier attached to a range that describes what a project exports, its public API surface, and what it imports, the external symbols it depends on but doesn't define. Monikers are the shared vocabulary that lets separate dumps talk to each other.

The linking mechanism follows directly from that: a database holding both dumps can match B's exported monikers against A's imported monikers, and where they match, a cross-repository jump becomes possible. Version 0.4.0 was the point where LSIF formally supported building a large system dump by dump, project by project, in reverse dependency order, and stitching the results together by linking result sets through their monikers. That was the moment LSIF stopped being a single-repo trick and started being pitched as an answer to organization-scale code navigation.

Where the moniker model breaks down in practice

The moniker idea is elegant on paper. In practice, it accumulates cracks at scale, which is exactly where a format like this is supposed to prove itself.

Versioning is the first crack. A moniker, to resolve correctly, needs to point not just at a symbol but at the right version of that symbol somewhere in another repository. Languages evolve, exporters get patched, and moniker schemes sometimes need to change to keep up. One fix is appending a version number onto the scheme value, but changing the schema itself breaks backward compatibility: dumps built under the old schema simply can't be linked against dumps built under the new one. Issues filed against Microsoft's lsif-node project document exactly this kind of incompatibility.

Parallelism is the second crack, and it's a structural one. LSIF dumps are naturally tempting to ingest in parallel, since a large dump has plenty of vertices and edges to divide across threads. But a thread handling one subsequence of the graph often needs context that lives in a different subsequence it hasn't processed yet. Workarounds exist, but they tend to introduce lock contention or other coordination costs that eat the very speedup parallel ingestion was supposed to deliver. This tension is a recognized structural challenge with the format's graph model.

Then there's the maintenance cost of the graph model in general. LSIF leans hard on numeric IDs to wire edges to vertices, and those IDs are opaque: a "6" or a "12" tells a developer nothing about what it points to without cross-referencing the rest of the dump. Sourcegraph, building indexers against the format, documented this as a real drag on debugging and on how fast new indexers could be developed.

Even the spec's own structure wasn't stable. Version 0.5.0 introduced a Group vertex meant to logically bundle projects together. Version 0.6.0 removed it. Storage implementors decided that grouping concept belonged in whatever backend stored the dump, not in the format describing the dump's contents. That kind of reversal, a feature added and then walked back one point release later, signals that the model underneath needed more than patching.

SCIP: the successor format that replaces LSIF's graph model with string-keyed symbols

Diagram: From LSIF to SCIP: The Architectural Break. Visualizes: Show a before/after or two-column comparison of LSIF vs SCIP across three concrete structural differences: (1) format — LSIF uses newline-separated JSON vs SCIP uses Protobuf schema…

SCIP, pronounced "skip" and standing for SCIP Code Intelligence Protocol, is Sourcegraph's answer to those accumulated cracks, and it's built to replace LSIF outright rather than patch it further.

The architectural break is total. SCIP drops LSIF's newline-separated JSON in favor of a Protobuf schema, and it drops opaque numeric IDs in favor of human-readable string identifiers for symbols. That single change collapses two of LSIF's separate concepts, monikers for cross-project linking and resultSet for grouping related ranges, into one unified symbol identifier that does both jobs at once. A symbol string in SCIP is legible on its own; it doesn't need the rest of the graph loaded into memory to mean something to a person reading it.

SCIP's lineage traces back to SemanticDB, a code indexing format that came out of the Scala ecosystem, and its design choices show that inheritance clearly: SemanticDB also favored durable, string-based symbol identifiers over the kind of numeric graph wiring LSIF used. Sourcegraph built out a Protobuf schema alongside it, with Go and Rust bindings carrying real utility functions, auto-generated TypeScript and Haskell bindings, and a command-line tool, the scip CLI, for working with index files directly.

How SCIP handles cross-repository navigation

Cross-repository navigation in SCIP works by following a symbol's string identifier straight to its definition in another repository. Cross-repository navigation in SCIP works by following a symbol's string identifier straight to its definition in another repository, a direct lookup rather than a graph traversal through a chain of opaque numeric IDs the way LSIF's moniker-linking required.

From a developer's chair, the experience is simple: click Go to Definition on an imported function, and the system resolves that reference against the SCIP index, landing on results that are compiler-accurate because the indexer analyzed the code semantically rather than guessing from text patterns. This design directly addresses the versioning fragility that dogged LSIF's monikers.

Getting there at scale requires an ingestion pipeline, and Sourcegraph's version of that pipeline runs language-specific indexers, scip-typescript, scip-python, scip-java among them, against checked-out code. Each indexer produces a SCIP index file, which is exactly the information a cross-repository lookup needs to land on the right definition rather than a same-named but different one.

SCIP indexer language coverage

Before SCIP existed, lsif.dev tracked the open-source LSIF indexer ecosystem, and its final snapshot is worth recording as a baseline, if only because that site is no longer maintained. Go had lsif-go from Sourcegraph, supporting cross-file and cross-repository navigation, marked Ready. Java had lsif-java, also from Sourcegraph, also Ready, also cross-file and cross-repository. C++, C, and a certain object-oriented C dialect were covered by an indexer, marked Beta, with cross-file and cross-repository support. Dart had two separate efforts: lsif_indexer from Workiva, Beta status with cross-file and cross-repository support, and lsif-dart from Sourcegraph, also Beta, but limited to doc hovers only. C# had an indexer from a community contributor, tcz717, called LsifDotnet, marked Development and noted as tested only on Windows. Haskell had hie-lsif from another contributor, mpickering, with status listed simply as Unknown.

That list, frozen in time, reads like an ecosystem still finding its footing, uneven maturity, gaps in platform testing, some indexers doing only part of the job. SCIP's indexer ecosystem picked up from there and includes tools like scip-typescript, scip-python, and scip-java, though the available material doesn't reproduce a status table as granular as lsif.dev's Ready/Beta/Development breakdown for the newer format.

The lsif.dev archive's own fate is itself a data point. A site whose entire purpose was tracking open-source LSIF indexers going unmaintained isn't a neutral fact; it's a marker that the community doing that building has moved its attention elsewhere. For a team weighing which format to invest in today, the practical read is straightforward: the active indexer development is happening on SCIP. LSIF tooling still exists, but it's legacy surface now, not where new engineering effort lands.

LSIF's design choices and their implications for teams building or choosing code intelligence today

LSIF's legacy isn't a story of failure so much as a story of a first attempt that named the problem correctly and got the solution partly right. Persisting a language server's knowledge into a storage-agnostic dump, so that go-to-definition and find-references work without a live process or a local clone, was and is the correct goal, and LSIF proved the concept was viable.

Where it struggled was in the mechanics: a graph of opaque numeric IDs is hard to debug, hard to parallelize, and brittle under the versioning demands that real, evolving, multi-repository codebases place on it. SCIP's string-keyed symbols are a direct response to each of those specific failure points. The indexer ecosystem has already consolidated around SCIP, and building against a format whose tooling is still under active development carries a different kind of risk than building against one whose community tracking site has gone quiet.

Sources

  1. LSIF Specification
  2. LSIF Specification
  3. LSIF Specification
  4. Overview
  5. LSIF.dev
  6. The Language Server Index Format (LSIF)
  7. sourcegraph.com
  8. sourcegraph.com

More in Code Navigation in IDEs