Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### graph-lite

- `0.0.1` — public header with version information only. The node/edge model,
- `0.0.1` — public surface established as a **C ABI** (`graph-lite.h`): C linkage,
opaque handles, integer status codes. `graph-lite.hpp` is header-only C++
ergonomics over it and exports nothing of its own. A C translation unit in the
suite is compiled as C and linked, so the header cannot stop being C-compatible
silently. Version and status-code entry points only; the node/edge model,
storage, traversal and persistence land in build phase 1.
([ADR 0004](docs/architecture/decisions/0004-graph-lite-c-abi.md))

[Unreleased]: https://github.com/developerz-ai/universal-lsp/commits/main
9 changes: 8 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ Two products, one repo, one direction of dependency.

| | What | Depends on |
|---|---|---|
| **graph-lite** | Embeddable property-graph library. Typed nodes, typed directed edges, properties. CSR storage, native traversal, mmap-friendly persistence. Single public header. MIT. | nothing |
| **graph-lite** | Embeddable property-graph library. Typed nodes, typed directed edges, properties. CSR storage, native traversal, mmap-friendly persistence. **Public surface is a C ABI** (`graph-lite.h`); `graph-lite.hpp` is header-only C++ ergonomics over it. MIT. | nothing |
| **Universal LSP** | The language server: stdio LSP + MCP + batch CLI, tree-sitter parsing, native scope/type engines, Lua plugins. Apache-2.0. | graph-lite |

**The doctrine that outranks instinct:**
Expand All @@ -68,6 +68,10 @@ Two products, one repo, one direction of dependency.
because something is not installed.
- **graph-lite knows nothing about this consumer.** No LSP concept in its API,
internals, tests, or docs. Both directions are enforced by guards; see below.
- **graph-lite's contract is C, not C++.** Every embedder — Ruby, JavaScript,
Python, Rust, Go — binds `graph-lite.h` through an FFI. No C++ type may cross
that boundary, and `graph-lite.hpp` is a convenience layered on top, never the
other way around ([ADR 0004](docs/architecture/decisions/0004-graph-lite-c-abi.md)).
- **Agents are the primary client, editors the compatible one.** Design request
shapes for "what breaks if I change this" as much as for hover-on-cursor.

Expand Down Expand Up @@ -151,6 +155,9 @@ bun run scripts/help.ts # the full catalog
service. This is the product's whole thesis.
- Never make graph-lite depend on anything in this repo — no include, no link, no
concept name.
- Never put a C++ type, an exception, or a published struct layout in
`graph-lite.h`. It is an ABI every binding pins; changing an entry point breaks
them silently, at load time.
- Never disable a guard, a warning, or a test to get green.
- Never introduce a feature flag or a second code path "for safety".
- Never commit a secret, a personal path, or a machine-specific value.
Expand Down
26 changes: 23 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ terminal-first, agent-driven world: standard **LSP over stdio** for editors, an
**MCP** front-end for agents, and a headless **batch-query CLI** for CI.

**graph-lite** — the missing *SQLite for graphs*: a small, dependency-free,
embeddable property-graph library any C++ project can drop in via a single header.
embeddable property-graph library any project can drop in via a single header.
Its public surface is a **C ABI**, so Ruby, JavaScript, Python, Rust, Go and C
embed it through their own FFI with no wrapper to keep in sync — the same thing
that gave SQLite its reach.
The LSP is its first and most demanding consumer — call graphs, import graphs and
symbol relationships across large codebases, under interactive latency — but
graph-lite's API is kept free of any of that vocabulary, and is
Expand Down Expand Up @@ -119,11 +122,28 @@ cmake -S graph-lite -B build/graph-lite -G Ninja
cmake --build build/graph-lite
```

```c
#include "graph-lite.h" /* the contract: C linkage, opaque handles, status codes */
```

```cpp
#include "graph-lite.h" // one header, no dependencies
#include "graph-lite.hpp" // C++ ergonomics over the same entry points, header-only
```

API and data model: [`docs/graph-lite/README.md`](docs/graph-lite/README.md).
```ruby
require "ffi" # and the same from Ruby, JS, Python, Rust, Go
module GraphLite
extend FFI::Library
ffi_lib "graph-lite"
attach_function :graph_lite_version_string, [], :string
end
```

A C translation unit in the test suite is compiled **as C** and linked, so a header
that stops being C-compatible fails our build instead of your integration.

API and data model: [`docs/graph-lite/README.md`](docs/graph-lite/README.md) ·
why C: [ADR 0004](docs/architecture/decisions/0004-graph-lite-c-abi.md).

## Languages at launch

Expand Down
14 changes: 10 additions & 4 deletions cmake/CompilerWarnings.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,30 @@
# `ulsp_warnings` everywhere instead.
add_library(ulsp_warnings INTERFACE)

# Applies to C and C++ alike. graph-lite's public header is C, and the test that
# proves it is a C translation unit — handing it C++-only flags produces a wall of
# "valid for C++ but not for C" noise that trains everyone to ignore the output.
target_compile_options(ulsp_warnings INTERFACE
-Wall
-Wextra
-Wpedantic
-Wshadow
-Wnon-virtual-dtor
-Wcast-align
-Wunused
-Woverloaded-virtual
-Wconversion
-Wsign-conversion
-Wdouble-promotion
-Wformat=2
# Position encoding, arena offsets and CSR indices are all integer maths on
# untrusted input. -Wconversion above is the one that catches the class of
# bug that panics a language server on an astral-plane codepoint.
-Wnull-dereference
-Wold-style-cast)
-Wnull-dereference)

# C++ only.
target_compile_options(ulsp_warnings INTERFACE
$<$<COMPILE_LANGUAGE:CXX>:-Wnon-virtual-dtor>
$<$<COMPILE_LANGUAGE:CXX>:-Woverloaded-virtual>
$<$<COMPILE_LANGUAGE:CXX>:-Wold-style-cast>)

if(ULSP_WERROR)
target_compile_options(ulsp_warnings INTERFACE -Werror)
Expand Down
84 changes: 84 additions & 0 deletions docs/architecture/decisions/0004-graph-lite-c-abi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# 0004 — graph-lite's embedding surface is a stable C ABI

**Status:** Accepted
**Date:** 2026-08-22

## Context

graph-lite is pitched as *the* embeddable graph library — the SQLite comparison is
not just about being in-process, it is about **who can embed it**. SQLite's reach
comes from a flat C ABI: every language runtime on earth can call C, and almost
none can call C++.

A C++-only surface limits graph-lite to C++ callers. That is a small fraction of
the projects that want in-process graph storage. Ruby wants it through FFI, Node
and Bun through N-API or `bun:ffi`, Python through `ctypes`/`cffi`, Rust through
`extern "C"`, Go through cgo — and every one of those needs the same thing: a
header with C linkage, opaque handles, integer error codes, and no C++ types
crossing the boundary.

C++ cannot provide that. It has no stable ABI across compilers or even across
standard-library versions; `std::string` in a signature makes the library
unlinkable from anything not built by the same toolchain, and unusable from any
non-C++ caller at all.

This is a **one-way door**, and it has to be decided before the API hardens: a C
ABI retrofitted onto a C++-shaped design is a second, worse API wrapping the
first, and every binding pins whichever one shipped.

## Decision

**The C ABI is graph-lite's primary public contract.** The C++ API is a
header-only convenience layer over it, not the other way around.

| Header | Language | Is |
|---|---|---|
| `graph-lite.h` | C (usable from C++) | **The contract.** `extern "C"`, opaque handles, integer error codes, caller-owned buffers. |
| `graph-lite.hpp` | C++ | Header-only ergonomics — RAII handles, spans, exceptions — implemented entirely in terms of `graph-lite.h`. Adds no exported symbol of its own. |

Rules that follow, and are not negotiable per-function:

- **No C++ type crosses the boundary.** No `std::string`, no `std::vector`, no
templates, no exceptions escaping, no `bool` with C++ semantics assumed.
- **Opaque handles**, never a struct whose layout callers depend on. Struct
layout in a public header is an ABI promise nobody wants to keep.
- **Integer error codes**, never a thrown exception and never a bare null that
collapses several failure reasons into one. Every failure a binding must
distinguish gets a distinct code.
- **Caller-owned buffers with explicit sizes**, or an allocation paired with an
explicit `graph_lite_free_*`. Bindings cannot free a pointer that a different
allocator produced.
- **No callbacks into the caller's runtime on a hot path.** A garbage-collected
runtime re-entering during a traversal is how a binding deadlocks.
- **Symbol-versioned and additive.** Removing or changing a C symbol breaks every
binding built against it, silently, at load time.

## Consequences

- Reach: any language with an FFI can embed it, which is the entire point of the
SQLite comparison.
- The C++ caller inside this repository uses `graph-lite.hpp` and pays nothing —
it is header-only and inlines away.
- Cost: designing in C is more work than designing in C++, and some ergonomics
(iterators, ranges) have to be expressed as cursor handles. Accepted; that cost
is paid once, by us, instead of by every binding author.
- **Reference bindings are a separate deliverable**, not part of the library.
Build phase 13 — they exist to prove the ABI is usable from outside, which is
the only honest test of a C surface designed by C++ programmers.
- A Lua binding would collide with the vocabulary guard, which bans `lua` as a
consumer dependency. If that becomes real, the answer is to revisit the ban
deliberately — not to add an allowlist entry.
- Error codes and handle semantics join the persisted layout and node id scheme on
the short list of things that cannot be walked back after the first release.

## Alternatives rejected

- **C++-only surface.** Simplest to write, and it caps adoption at C++ projects.
It also makes the library unlinkable across toolchains, which embedders hit
immediately.
- **C++ core with a C wrapper added later.** The wrapper ends up shaped by C++
decisions it cannot undo — string ownership, exception-based errors, layout
assumptions — and bindings inherit all of it.
- **A serialised RPC/IPC surface (JSON, protobuf) instead of an ABI.** Defeats
the purpose: the whole value is being in-process, with no serialisation between
the caller and the graph.
1 change: 1 addition & 0 deletions docs/architecture/decisions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ the reasoning survives the reversal.
| [0001](0001-record-architecture-decisions.md) | Record architecture decisions | Accepted |
| [0002](0002-graph-lite-lives-here-under-mit.md) | graph-lite develops in this repo, under its own license and version | Accepted |
| [0003](0003-no-external-language-servers.md) | No external language-server processes, ever | Accepted |
| [0004](0004-graph-lite-c-abi.md) | graph-lite's embedding surface is a stable C ABI | Accepted |

## When to write one

Expand Down
32 changes: 24 additions & 8 deletions docs/graph-lite/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
# graph-lite

A small, fast, dependency-free, embeddable property-graph library for C++20. One
public header, one static library, no server, no query language, no runtime
A small, fast, dependency-free, embeddable property-graph library. One public
header, one static library, no server, no query language, no runtime
dependencies.

**The public surface is a C ABI.** Anything with an FFI can embed it — C, C++,
Ruby, JavaScript, Python, Rust, Go — and C++ callers get a header-only ergonomics
layer over the same entry points. The implementation language is C++; the contract is
not, because C++ has no stable ABI across compilers or standard library versions.

> **Status: pre-implementation (v0.0.1).** The header currently exposes version
> information only. The model, storage, traversal and persistence described below
> are the design being built — not shipped behaviour. Design docs date from
Expand Down Expand Up @@ -76,19 +81,30 @@ disproportionate testing investment relative to the traversal algorithms:
store is either the old state or the new one and never a third thing.
- The suite is expressed purely in the generic node/edge vocabulary above.

## The surface

| Header | For | Is |
|---|---|---|
| `graph-lite.h` | C, and every FFI | **The contract.** C linkage, opaque handles, integer status codes, caller-owned buffers, additive only. |
| `graph-lite.hpp` | C++ | Header-only ergonomics over the C header. Exports no entry point of its own. |

Rules the C surface holds to — no caller-visible type from the implementation
language, opaque handles rather than published struct layouts, a distinct status
code per failure a caller must distinguish, explicit buffer ownership, and no
callback into the caller's runtime on a hot path. Full rationale:
[`../architecture/decisions/0004-graph-lite-c-abi.md`](../architecture/decisions/0004-graph-lite-c-abi.md).

The claim is tested rather than asserted: `tests/graph-lite/c_abi_test.c` is
compiled **as C** and linked into the suite, so a header that stops being
C-compatible fails the build instead of failing an embedder.

## Embedding

```bash
cmake -S graph-lite -B build/graph-lite -G Ninja
cmake --build build/graph-lite
```

```cpp
#include "graph-lite.h"

const auto v = graph_lite::version(); // check against GRAPH_LITE_VERSION_* macros
```

Or vendor the directory and `add_subdirectory(graph-lite)`. It configures
standalone, so neither path needs anything else from the repository it currently
lives in.
Expand Down
4 changes: 4 additions & 0 deletions docs/graph-lite/data-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ Ids being stable across a save/load cycle is a **one-way door**: callers will
persist them in their own state the moment the feature exists, so the guarantee
has to be decided before the first release, not after.

Ids cross the C boundary, so their width is ABI. Pick a fixed-width integer type
and never change it — a binding that read 64 bits and now reads 32 does not fail
to compile, it reads garbage.

## Edges

An edge is directed, typed, and carries properties of its own.
Expand Down
4 changes: 3 additions & 1 deletion docs/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ The Universal LSP is graph-lite's first, heaviest, and most demanding real-world

### 5.1 Scope and API shape

- Public interface exposed via a single header (conceptually `graph-lite.h`, mirroring the ergonomics of single-header/amalgamation-style C/C++ libraries like SQLite's), so any C++ project can embed graph-lite with minimal integration friction.
- Public interface exposed via a single header (`graph-lite.h`, mirroring the ergonomics of single-header/amalgamation-style C/C++ libraries like SQLite's), so any project can embed graph-lite with minimal integration friction.
- **That header is C, with C linkage, and it is the contract** — opaque handles, integer status codes, caller-owned buffers, no C++ type crossing the boundary. The SQLite comparison is not only about being in-process: SQLite's reach comes from a flat C ABI that every language runtime can call. Ruby (FFI), JavaScript (N-API / `bun:ffi`), Python (`ctypes`/`cffi`), Rust, Go and C all bind the same entry points. A C++-only surface would cap adoption at C++ projects and would not even link reliably across toolchains, since C++ has no stable ABI. A separate header-only `graph-lite.hpp` provides C++ ergonomics over the same entry points and exports nothing of its own. Rationale and the rules that follow: [`docs/architecture/decisions/0004-graph-lite-c-abi.md`](architecture/decisions/0004-graph-lite-c-abi.md).
- Generic property-graph data model: typed nodes and typed, directed edges, both carrying arbitrary key/value properties. No built-in concept of "symbol," "file," or "function" — those are just node types a consumer defines.
- Core operations: add/remove nodes and edges, traverse (BFS/DFS, bounded and unbounded depth), reverse-edge traversal (for blast-radius/impact-style queries), shortest/any path between two nodes, and basic filtering by node/edge type during traversal.
- No query language in the initial version (no Cypher/Datalog/SQL layer) — the library exposes a programmatic C++ traversal API. A declarative query layer can be considered later, once real usage clarifies what's actually needed, rather than designed speculatively up front.
Expand All @@ -64,6 +65,7 @@ The Universal LSP is graph-lite's first, heaviest, and most demanding real-world
### 5.3 Testing

- graph-lite gets its own independent test suite, expressed purely in terms of its generic node/edge model — no LSP concepts should appear anywhere in graph-lite's tests, as a forcing function for API cleanliness.
- The C ABI claim is tested, not asserted: at least one translation unit in the suite is compiled **as C** and linked, so a header that stops being C-compatible fails the build rather than failing an embedder. Reference bindings in a scripting language (build phase 13) are the honest test of whether a C surface designed by C++ programmers is actually usable from outside.
- Correctness testing should include fuzzing and crash-consistency testing of the persistence layer specifically — this is the highest-risk area for any embedded storage engine (silent data corruption is a severe failure mode) and deserves disproportionate testing investment relative to the traversal algorithms themselves.

## 6. Universal LSP — high-level architecture
Expand Down
18 changes: 14 additions & 4 deletions graph-lite/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ allowlist entry, and never a looser rule.
## Layout

```
include/graph-lite.h the ONE public header — additions here are a public contract
include/graph-lite.h THE CONTRACT — C linkage. Every binding pins this.
include/graph-lite.hpp header-only C++ ergonomics over it; exports nothing
src/ implementation
CMakeLists.txt configures standalone AND as a subdirectory
CMakePresets.json standalone preset; ships on extraction
Expand All @@ -38,9 +39,18 @@ LICENSE MIT, deliberately not the engine's license

## Conventions

- **The header is a one-way door.** Anything added to `graph-lite.h` is a promise:
node ids, the persisted layout, and every exported signature. Think before, not
after.
- **The C header is a one-way door.** Anything added to `graph-lite.h` is a
promise: node ids, the persisted layout, status code numbers, and every exported
signature. A foreign binding pins them and breaks silently at load time.
- **No C++ type crosses the C boundary** — no `std::string`, no `std::vector`, no
template, no escaping exception, no published struct layout. Opaque handles,
integer status codes, caller-owned buffers with explicit sizes.
- **Status codes are numbers, and numbers are ABI.** Append; never renumber, never
repurpose. `graph_lite_status_string()` must never return NULL, including for a
code from a newer header than the linked library.
- `tests/graph-lite/c_abi_test.c` is compiled **as C**. If it stops compiling, the
header stopped being usable from every non-C++ caller — fix the header, not the
test.
- Version this library independently — `GRAPH_LITE_VERSION` in `CMakeLists.txt`
and the macros in the header move together, and neither tracks the engine.
- Tests live in `tests/graph-lite/`, expressed purely in the generic model. If a
Expand Down
3 changes: 3 additions & 0 deletions graph-lite/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ endif()
# Versioned independently of the LSP engine, per docs/spec.md §2.
set(GRAPH_LITE_VERSION 0.0.1)

# The public surface is C (graph-lite.h); graph-lite.hpp is header-only
# ergonomics over it and exports nothing. Sources may be C++ internally — the ABI
# is what is constrained, not the implementation language.
add_library(graph-lite
src/version.cpp)
add_library(graph-lite::graph-lite ALIAS graph-lite)
Expand Down
Loading