Implementing a binding¶
This document is the deep-dive companion to
embedding.md and session.md. Those
two cover the broad shape of the engine and the canonical Cypher
pipeline. This document covers the concerns that surface specifically
when you sit down to write a new wrapper — error mapping, embedder
implementation, dataset wrapping patterns, the bridge-layer choice.
If you only want to embed kglite from a Rust binary, start with
embedding.md and stop there. If you want to
publish a Go, JavaScript, JVM, or other-language binding that other
people will depend on — read this one.
Reference implementations¶
KGLite ships three working bindings. They are the canonical worked examples; every pattern in this guide is grounded in one of them.
Binding |
Path |
Audience |
Bridge style |
|---|---|---|---|
|
|
|
PyO3 (Rust → CPython C ABI) |
|
|
Anything that talks the Neo4j Bolt protocol — |
Network protocol (no in-process binding; you connect over TCP) |
|
|
LLM agents speaking the Model Context Protocol |
Network protocol (stdio or TCP) |
When in doubt about an unfamiliar concern, the right move is to grep the existing binding crates for how they handled it. They are the deployed answer.
The bridge-layer choice¶
Your binding needs to get values across the language boundary. KGLite the engine is a Rust library — it doesn’t know your language exists. You have three options:
Option 1 — Rust-to-Rust direct (no bridge)¶
The simplest case. Your “binding” is another Rust crate (a CLI tool,
a custom server, a worker process). You depend on kglite directly,
call kglite::api::* functions, get back native Rust types. No FFI,
no marshalling, no glue.
[dependencies]
kglite = "0.11"
Everything in this guide still applies, but the FFI sections are informational rather than required.
The bolt-server crate is the canonical worked example of this style.
It depends on kglite directly, wraps Session in its own
connection-state struct, and never crosses a language boundary.
Option 2 — Language-specific FFI (the common case)¶
You’re writing a Go binding (cgo), a Node binding (napi-rs), a JVM binding (jni-rs), a .NET binding (com-rs), etc. Each language has a runtime-specific Rust crate that handles the lowest-level marshalling.
Your binding becomes a Rust crate that:
Depends on
kglite(the engine) and on the language-specific FFI crate (napi,jni,pyo3, etc.).Wraps
kglite::api::*functions with the FFI macros for the target runtime.Builds as a
cdylib(or whatever your runtime wants) and is loaded into the host process.
The pyapi crate (crates/kglite-py/) is the canonical worked example.
It’s a 5k-line crate that wraps everything you’d want from Python:
KnowledgeGraph PyClass, Selection PyClass, NumPy/Pandas conversion,
async/GIL handling, the works. A Go binding aiming for similar
completeness would be similar size.
For the smaller-bridge approach (just the engine, no ergonomics) the
crate is more like 1k lines — see the cgo sketch in
embedding.md.
Option 3 — Use the kglite-c crate (the canonical non-Rust path)¶
crates/kglite-c/ exposes kglite::api::* through a stable C ABI.
Every language with FFI (Go, JavaScript, JVM, .NET, …) can bind to
kglite through a single C header without writing Rust.
The conventions are fixed in c-abi.md; the short
version:
kglite_prefix on every function.Opaque-handle pattern:
KgliteGraph*/KgliteSession*/KgliteCypherResult*/KgliteEmbedder*/KgliteSecClient*.Errno-style errors: every fallible function returns
KgliteStatusCode(0 = OK) with out-parameters for both the result handle and an owned error-message string. 16 status variants map 1:1 toKgErrorCode; three are C-ABI-specific (KGLITE_STATUS_CODE_INVALID_UTF8,_NULL_POINTER,_OUT_OF_MEMORY).Memory: caller frees every
*mut Thandle via the type’skglite_<type>_free; every owned out-string via the singlekglite_free_string.Sync-only: bindings own their async/threading. Async dataset fetchers expose
*_blockingcompanions.JSON-at-boundary: parameters in, rows out, dataset reports out — all as JSON strings. Caller parses with their language’s stdlib JSON facility.
Setup¶
# Cargo.toml of an in-Rust consumer
[dependencies]
kglite-c = "0.13" # cdylib + staticlib + rlib
For non-Rust consumers, link against libkglite_c.{so,dylib,dll}
and include the header that ships at
crates/kglite-c/include/kglite.h. Build the platform library from
the kglite-c source crate with cargo build --release -p kglite-c.
Precompiled C ABI libraries are not currently attached to releases; the
generated header is committed in the repository and included with the source
crate.
Worked example — cgo (Go)¶
package kglite
/*
#cgo LDFLAGS: -lkglite_c
#include <stdlib.h>
#include "kglite.h"
*/
import "C"
import (
"encoding/json"
"errors"
"fmt"
"unsafe"
)
type Graph struct {
handle *C.KgliteGraph
}
func LoadFile(path string) (*Graph, error) {
cpath := C.CString(path)
defer C.free(unsafe.Pointer(cpath))
var graph *C.KgliteGraph
var errMsg *C.char
rc := C.kglite_load_file(cpath, &graph, &errMsg)
if rc != C.KGLITE_STATUS_CODE_OK {
defer C.kglite_free_string(errMsg)
return nil, fmt.Errorf("load_file: %s", C.GoString(errMsg))
}
return &Graph{handle: graph}, nil
}
func (g *Graph) Close() {
C.kglite_graph_free(g.handle)
g.handle = nil
}
type Session struct {
handle *C.KgliteSession
}
func (g *Graph) NewSession() (*Session, error) {
var sess *C.KgliteSession
rc := C.kglite_session_new(g.handle, &sess)
if rc != C.KGLITE_STATUS_CODE_OK {
return nil, errors.New("session_new failed")
}
// Graph ownership moved into the session.
g.handle = nil
return &Session{handle: sess}, nil
}
func (s *Session) Cypher(query string, params map[string]any) ([]map[string]any, error) {
cquery := C.CString(query)
defer C.free(unsafe.Pointer(cquery))
var cparams *C.char
if params != nil {
paramJSON, _ := json.Marshal(params)
cparams = C.CString(string(paramJSON))
defer C.free(unsafe.Pointer(cparams))
}
var result *C.KgliteCypherResult
var errMsg *C.char
rc := C.kglite_session_execute_read(s.handle, cquery, cparams, &result, &errMsg)
if rc != C.KGLITE_STATUS_CODE_OK {
defer C.kglite_free_string(errMsg)
return nil, fmt.Errorf("execute_read: %s", C.GoString(errMsg))
}
defer C.kglite_cypher_result_free(result)
rowsJSON := C.kglite_cypher_result_rows_json(result)
defer C.kglite_free_string(rowsJSON)
var rows []map[string]any
if err := json.Unmarshal([]byte(C.GoString(rowsJSON)), &rows); err != nil {
return nil, err
}
return rows, nil
}
Worked example — napi-rs (Node.js)¶
// crates/kglite-js/src/lib.rs (in a hypothetical kglite-js crate)
use napi::bindgen_prelude::*;
use napi_derive::napi;
use std::ffi::CString;
#[napi]
pub struct Graph {
inner: *mut kglite_c::KgliteGraph,
}
#[napi]
impl Graph {
#[napi(factory)]
pub fn load_file(path: String) -> Result<Self> {
let c_path = CString::new(path).map_err(|e| Error::from_reason(e.to_string()))?;
let mut graph: *mut kglite_c::KgliteGraph = std::ptr::null_mut();
let mut err_msg: *const std::ffi::c_char = std::ptr::null();
let rc = unsafe {
kglite_c::kglite_load_file(
c_path.as_ptr(),
&mut graph as *mut _,
&mut err_msg as *mut _,
)
};
if rc != kglite_c::KgliteStatusCode::Ok {
let msg = unsafe { std::ffi::CStr::from_ptr(err_msg) }.to_string_lossy().to_string();
unsafe { kglite_c::kglite_free_string(err_msg) };
return Err(Error::from_reason(msg));
}
Ok(Self { inner: graph })
}
// ... session / cypher methods follow the same shape ...
}
impl Drop for Graph {
fn drop(&mut self) {
if !self.inner.is_null() {
unsafe { kglite_c::kglite_graph_free(self.inner) };
}
}
}
A pure-JS consumer (no Rust at all) can link against
libkglite_c.so via ffi-napi or koffi, calling the C
functions directly with the same shapes.
Worked example — JNI (JVM)¶
// Java / Kotlin / Scala — through a JNI shim crate. The shim
// is ~500 LOC of jni-rs glue similar to the cgo shape above.
public class Graph implements AutoCloseable {
private long handle; // opaque *mut KgliteGraph
public static Graph loadFile(String path) {
return new Graph(KgliteJni.loadFile(path));
}
public Session newSession() {
return new Session(KgliteJni.sessionNew(handle));
// handle is moved into session; do not close graph after this.
}
@Override
public void close() {
if (handle != 0) {
KgliteJni.graphFree(handle);
handle = 0;
}
}
}
The JNI native crate would call kglite_c::kglite_* functions
through jni-rs bindings, mapping the JVM long handles to
*mut KgliteX round-trip casts. Cypher result rows come back as
JSON; the Java side parses with Jackson / Gson / JsonParser.
What kglite-c hands you (v1 surface)¶
Lifecycle:
load_file,save_graph,graph_free.Session:
session_new,session_execute_read,session_execute_mut,session_free. Plussession_set_embedder.Result:
cypher_result_columns_json,cypher_result_rows_json,cypher_result_row_count,cypher_result_free.Error introspection:
status_code_name,status_code_neo4j_status,status_code_http_status.Embedder:
embedder_fastembed_new(feature-gated),embedder_free,session_set_embedder.ABI version:
kglite_abi_version()for startup checks.
Phase H built out this surface across several sub-phases (H.2 skeleton, then embedder + the rest). The pre-packaged dataset loaders (SEC EDGAR, Sodir, Wikidata) are no longer part of the kglite core — they live in the separate kglite-datasets project and are not exported through the C ABI; kglite loads the graphs those loaders produce via the ordinary lifecycle functions.
Alternatives if kglite-c doesn’t fit¶
Use the existing PyO3 wrapper indirectly (your binding calls Python which calls kglite — slow and weird, but works today).
Use the network protocols. If your binding is HTTP/RPC-shaped, the Bolt server or MCP server are already canonical wire formats — your “binding” becomes a Bolt/MCP client in your target language, zero compiled code required.
Error mapping¶
Every kglite API call returns Result<T, KgError>. KgError is a
typed enum with 17 variants; each variant has a stable
KgErrorCode discriminant. Your binding maps these to its target
language’s idiomatic error types. The file-I/O variants are worth
surfacing distinctly: FileNotFound, FileFormat (corrupt /
truncated / wrong-format .kgl — what load_file / load_kgl_bytes
return on a bad file), and FileIo (permission, mid-read) — so a
consumer can tell “rebuild from source” from “create new”.
The table below is the recommended mapping. The “Recoverable?” column is from the agent’s POV — should the binding’s caller retry? rewrite? give up?
|
When it fires |
Recoverable? |
Suggested language idiom |
|---|---|---|---|
|
Tokenizer / parser rejected the query string |
No — the query is malformed |
Type/usage error ( |
|
Query exceeded its |
Maybe — retry with longer budget or rewrite |
Timeout error ( |
|
Caller flipped |
No — the caller asked to stop |
Interrupt / cancel idiom ( |
|
Mutation conflict, predicate panic, etc. |
Sometimes — context-dependent |
Runtime error |
|
A param was the wrong type for an operator |
No — fix the call site |
Type error |
|
Query references unknown label/property/index |
No — schema mismatch |
Validation error |
|
Bulk-mutation row failed validation (FK, unique, type) |
No for that row — others may succeed |
Validation error per row |
|
Blueprint expression failed to evaluate |
No — fix the expression |
Validation error |
|
Lookup by ID/name returned nothing |
No — caller’s domain logic |
KeyError / NotFoundError |
|
Same, for edges |
No |
KeyError / NotFoundError |
|
Property doesn’t exist on the node |
No |
KeyError / NotFoundError |
|
Path passed to |
No |
FileNotFoundError / IOError |
|
File exists but isn’t a valid |
No |
Format / parse error |
|
Filesystem error during read/write |
Maybe — retry on transient (disk full, network FS) |
IO error |
|
Function argument was wrong (out of range, malformed) |
No |
ValueError / IllegalArgumentException |
|
Required argument was None / null |
No |
TypeError / NullPointerException |
|
Should-never-happen invariant violation |
No — report as bug |
InternalError |
KgError::Display already produces a human-readable message; bindings
typically expose both the code (for programmatic dispatch) and the
message (for the user). The pyapi crate’s crates/kglite-py/src/error_py.rs is the
canonical mapping — every Python exception type kglite raises is a
mechanical projection of KgErrorCode.
The bolt-server’s crates/kglite-bolt-server/src/error_map.rs maps each variant into a
Bolt ClientError with a Neo4j-style status code prefix
(Neo.ClientError.Schema.SchemaNotFound, etc.). Use it as a
reference if your binding needs wire-protocol-shaped errors.
Implementing the Embedder trait¶
KGLite’s text_score() Cypher function needs to embed user queries
at lookup time. The engine doesn’t ship a specific embedder — you
plug in your own via the kglite::api::Embedder trait.
The trait has two required methods plus three optional ones:
pub trait Embedder: Send + Sync {
fn dimension(&self) -> usize;
fn embed(&self, texts: &[String]) -> Result<Vec<Vec<f32>>, String>;
fn model_id(&self) -> Option<String> { None } // optional (provenance)
fn load(&self) -> Result<(), String> { Ok(()) } // optional
fn unload(&self) {} // optional
}
The contract:
dimension— the output vector size (e.g. 384 for all-MiniLM, 1024 for bge-m3). The engine validates this against user-supplied vectors atset_embeddings()time, so it has to be fixed for the embedder’s lifetime.embed(texts)— embed a batch. Return one vector per input text, each of lengthdimension(). Errors go back to the user viaKgError::CypherExecution.model_id— optional (defaults toNone). The model’s stable id (e.g."BAAI/bge-m3"); when present it’s stamped onto the embedding store as provenance and surfaced viaembedding_info(). Implement it if your embedder can name its model.load/unload— optional lifecycle hooks. Called before / after each embedding pass. Use these for lazy model loading + idle cooldown if your embedder is expensive to keep resident.
Two existing implementations to copy from¶
FastEmbedAdapter (crates/kglite/src/graph/embedder/fastembed.rs)
— wraps the fastembed-rs crate for local ONNX inference. ~200 lines,
gated on the fastembed Cargo feature. Concrete implementation of
lazy load + idle unload with a cooldown timer.
PyEmbedderAdapter (in crates/kglite-py/src/graph/embedder/py_adapter.rs) — wraps
a user-supplied Python class implementing the embedder protocol. The
adapter acquires the GIL for embed() calls and translates between
Python lists and Rust vectors. Showed up as the pattern for any
binding that wants to let users plug in their own embedder.
Pattern: HTTP-API-backed embedder¶
If your binding wants to wrap a hosted embedding API (OpenAI, Cohere, Voyage, etc.):
use kglite::api::Embedder;
use std::sync::Mutex;
pub struct OpenAiEmbedder {
api_key: String,
model: String, // "text-embedding-3-small" → 1536
dimension: usize,
client: reqwest::blocking::Client,
}
impl Embedder for OpenAiEmbedder {
fn dimension(&self) -> usize { self.dimension }
fn embed(&self, texts: &[String]) -> Result<Vec<Vec<f32>>, String> {
// POST to /v1/embeddings, parse the response, return.
// The session module passes one batch at a time; you don't
// need to chunk further unless your API limits batch size.
// …
}
// No load/unload — HTTP client is cheap to keep around.
}
Your binding then exposes a constructor in its native language
that returns Box<dyn Embedder> (or wraps it in the binding’s
own embedder type, with Arc<dyn Embedder> underneath).
Loading data¶
Four ways to populate a DirGraph. Bindings expose whichever subset
fits their audience.
1. Read a .kgl file written by any other binding¶
use kglite::api::load_file;
let graph = load_file("snapshot.kgl")?; // → Arc<DirGraph>
.kgl is the cross-binding portable format. A graph saved by Python
(kg.save("snapshot.kgl")) reads cleanly here. A graph saved by your
Go binding will read in Python. The format is versioned (v3, v4)
and the engine handles version negotiation transparently.
2. Build from a blueprint + CSVs¶
The blueprint is a JSON/YAML schema spec; the engine reads it, streams the referenced CSVs, materializes nodes + connections.
use kglite::api::blueprint::{load_blueprint_file, build, Blueprint};
use kglite::api::DirGraph;
use std::path::Path;
let blueprint: Blueprint = load_blueprint_file(Path::new("schema.json"))?;
let mut graph = DirGraph::new();
let report = build(&mut graph, &blueprint, Path::new("./data/csv/"))?;
println!("built {} nodes, {} connections", report.nodes_in, report.connections_in);
Blueprint carries the full schema spec; BuildReport carries
counts + validation errors. The Python wheel’s from_blueprint
wraps these two functions with path resolution + lock_schema + a
PyO3-flavored progress callback (see
crates/kglite-py/src/graph/pyapi/blueprint.rs:23); your binding
will likely want the same shape in its own language idiom.
3. Build from a source tree (code intelligence)¶
use codingest::build_code_tree; // the standalone builder crate
let graph = build_code_tree(Path::new("./my_project/"), /* options */)?;
This walks a source tree with tree-sitter parsers and produces a code-intelligence graph (Function / Class / Module / etc. nodes, CALLS / DEFINES / IMPORTS edges). The kglite MCP server uses this to answer “what functions call X” queries about a codebase.
4. Load RDF / N-Triples¶
use kglite::api::load_file; // plus the RDF/N-Triples loaders
The kept RDF loaders (load_rdf / load_ntriples) ingest Turtle /
N-Triples / N-Quads / TriG directly, including Wikidata-scale
latest-truthy dumps streamed straight into a disk-backed graph.
Reach for these when your binding builds a graph from RDF sources.
The pre-packaged dataset loaders (SEC EDGAR, Sodir, Wikidata) are no
longer part of the kglite core API surface — they live in the
separate kglite-datasets project. kglite loads the graphs those
loaders produce via the ordinary lifecycle API (load_file), so a
binding that wants that data either reads a pre-built .kgl /
disk-graph or binds to kglite-datasets separately.
Wrapping a graph source for your binding¶
The boundary rule¶
Before any code: there is one rule about where things go.
A wrapper only contains code that is specific to its environment and cannot be used by any other sibling wrapper. Anything that two or more wrappers would write identically belongs in
kglite::api::*.
That rule is the lens for every wrapper decision. PyO3 marshalling
goes in the Python wrapper because no Go binding would use it. A
generic JSON/RDF parser goes in core because every binding parses
the same input. A tqdm progress display goes in the Python wrapper
because Go uses channels and JS uses event emitters for the same
purpose. A cache-freshness check (“is the local file older than the
remote one”) goes in core because every binding asks the same
question the same way.
If you find yourself writing logic in a wrapper that another binding would copy verbatim, stop and file it as a core lift. (The pre-packaged dataset loaders were the original worked example of this doctrine; they have since been extracted wholesale into the separate kglite-datasets project, so they are no longer part of the core API a binding wraps.)
The audit history¶
The kglite::api surface was sized against the wheel’s PyO3 methods
in May 2026 (the binding-framework sprint that produced 0.10.0
through 0.10.3). The current shape reflects which items every
binding would write identically vs which are tailored to one
environment.
Binding-side patterns cookbook¶
Patterns that surface in every binding, with notes on how the existing bindings handle them.
Process-local cache¶
Loading a large graph (a Wikidata-scale dump at 1.4B triples) takes minutes. In Jupyter or any REPL-like environment, the user wants a re-opened graph to return the same instance on re-execution instead of reloading.
A binding does this via a module-level cache keyed on the load
parameters (e.g. (path, entity_limit)). Each binding has its own
idiom:
Language |
Idiom |
|---|---|
Python |
Module-level |
Go |
|
JS / Node |
Module-level |
JVM |
|
Rust binary |
|
Cache invalidation key should include anything that affects what gets loaded — workdir path, entity limits, source-file mtime if relevant.
Lazy result materialization¶
Cypher queries can return millions of rows. The session module’s
execute_read returns a fully-materialized CypherResult by
default — fine for small results, ruinous for large ones.
For large results, the engine has a lazy path: set
ExecuteOptions.lazy_eligible = true and the returned
CypherResult will have a lazy: Some(LazyResultDescriptor).
Your binding’s result-iteration code then calls back into the
engine row-by-row instead of materializing upfront.
The Python wheel’s ResultView
(crates/kglite-py/src/graph/pyapi/result_view.rs) is the canonical
lazy materializer. Bolt-server’s ResultStream
does the same thing for the wire protocol. If your binding doesn’t
have a lazy materializer, pass lazy_eligible = false — otherwise
you’ll silently see empty result.rows for any non-ORDER BY
query.
Value type conversion¶
kglite::api::Value is the engine’s universal value type (scalar
variants + List + Map + graph-specific Node / Relationship /
Path). Your binding needs to translate at the FFI boundary.
The pattern in every existing binding:
Define a binding-native value type (
PreProcessedValuein pyapi,BoltValuein bolt-server, JSON in mcp-server).Write a
From<Value> for YourValueimpl (and reverse if your binding accepts user-supplied params).Pattern-match every
Valuevariant. Forgetting one is a runtime error.
The pyapi crate’s crates/kglite-py/src/datatypes/py_out.rs is the
densest example — it handles NumPy and Pandas type coercion on top
of basic conversion.
Progress callbacks¶
Long-running operations (dataset fetch, blueprint build) take seconds-to-minutes. Users want progress.
The engine’s BuildReport carries final counts but doesn’t emit
per-row progress. Bindings emit their own progress by:
Wrapping the fetch / build call in a thread that periodically reads a shared counter
Or calling small batches in a loop and reporting between them
The Python wheel uses tqdm with explicit batch-size control on
the fetch side and a verbose=True flag for build-time prints. A
Go binding might use a chan struct{} for progress events with
a separate goroutine consuming them.
This is genuinely binding-specific — no shared abstraction is likely to help. Pick the idiom your audience expects.
Cancellation / interruptible queries¶
ExecuteOptions.cancel: Option<&AtomicBool> is the engine-agnostic
cancellation primitive. The engine polls it at the same checkpoints it
polls the query deadline (pattern-matcher scans + expansions); once the
flag is set, the run aborts and the call returns KgError::Cancelled
(KgErrorCode::Cancelled → HTTP 499, Neo
Neo.ClientError.Transaction.Terminated). Leave it None
(ExecuteOptions::eager does) and queries are deadline-bounded only.
How you flip the flag is binding-specific — it’s part of the async/threading/signal model each binding owns:
The Python wheel installs a scoped
SIGINT(Ctrl-C) handler for the duration of a query that flips a process-globalAtomicBool, then restores the previous handler;KgError::Cancelledis mapped to Python’s builtinKeyboardInterrupt. (Read paths only; mutations stay deadline-bounded.)A server binding typically wires the flag to a request-cancellation token (client disconnect, gRPC
tokio::select!on the cancel future, a deadline-exceeded watcher) and mapsCancelledonto its protocol’s cancel/abort status.
Because cancel is a &AtomicBool, the flag must outlive the
execute_* call — a 'static global (the wheel’s choice) or a flag
owned by the request scope both work. The previous SIGINT disposition,
or whatever you replaced, is yours to restore.
What you don’t need to write¶
The work that’s already done. Your binding inherits all of this
by depending on kglite:
Component |
Where it lives |
LOC |
|---|---|---|
Cypher parser + planner + executor |
|
~15,000 |
Snapshot/working CoW + OCC |
|
~1,500 |
Schema validation pipeline |
|
~800 |
Blueprint loader + builder |
|
~5,000 |
|
|
~3,000 |
Code-graph builder (tree-sitter) |
the |
~4,000 |
RDF / N-Triples loaders |
|
— |
Embedder trait + FastEmbed adapter |
|
~600 |
Cypher result types + path/node/rel |
|
~2,000 |
Your binding contributes:
Component |
Notes |
|---|---|
Language-native graph handle wrapping |
The thin facade your users hold |
Value type marshalling ( |
Mechanical but error-prone — write tests |
Error mapping ( |
Use the table above |
Lifecycle (open, close, save, snapshot) |
Thin wrapper around the engine functions |
Optional: lazy result iterator |
If your binding has any |
Optional: embedder wrapper for user-provided embedders |
If your binding lets users plug in custom embedders |
The hard parts — correctness of Cypher, snapshot isolation, OCC conflict detection, format portability — you inherit.
Cross-binding portability checklist¶
A .kgl file written by your binding should load cleanly in any
other binding. To stay portable:
Use
kglite::api::save_graph(not your own format). It’s versioned, checksummed, and bumps along with the engine.Don’t bundle binding-ergonomic state in the graph itself. Selection caches, default timeouts, progress callbacks — these are per-binding overlays. Save the
DirGraph, let each binding add its own state on load.If you write custom values into Map / List, stick to the scalar variants
Valuealready supports. Don’t smuggle a JSON string in expecting another binding to parse it; use a proper variant.Validate after save. Round-trip your test fixtures (your-binding →
.kgl→ Python →.kgl→ your-binding) at least once per release. Thetests/test_phase4_parity.pysuite is the model.
Roadmap¶
What the binding-author experience needs that isn’t done yet, roughly in priority order. File issues for the ones that block your specific binding:
Phase H — a
kglite-ccrate exposing the engine through a stable C ABI. Unlocks bindings in any FFI-capable language without each one rolling its ownextern "C"layer.Selection / fluent-API in core. Today’s wheel exposes a fluent
select() / where() / sort()builder; unclear how much of the builder lives in core vs. the PyO3 layer. Punted unless a binding asks for it.Result streaming via the C ABI. If/when Phase H lands, there’s a design question about how to stream the lazy materializer across a C boundary.
Graph algorithms exposed in
api::*. Shortest path, centrality, community detection — what’s implemented internally vs. aspirational needs verification (audit punchlist item #8).
If you build a binding and discover a real gap, file an issue — that’s the right signal for promoting an item from the maintainer’s deferred-items list into actual work.
See also¶
embedding.md— full embedder guide, polars- style split rationale, quick start.session.md— canonical Cypher pipeline + CoW transaction reference.api-reference.md— manifest of stable surface items with semver guarantees.Cypher reference — the Cypher subset kglite supports.
CLAUDE.md(repo root) — engineering conventions for changes to the engine itself.