A hypergraph fact store with Datalog queries, backed by LMDB and roaring bitmaps.
Everything is stored as facts — hyperedges connecting typed entities:
{"edges": [["player", "Alice"], ["team", "Rockets"], ["rel", "plays_for"]], "source": "league"}Query from any angle:
kb get player/Alice # What team is Alice on?
kb get team/Rockets # Who plays for the Rockets?
kb get rel/plays_for # All player-team relationshipszig build # Requires Zig 0.15.2kb ingest data.jsonl # Load facts from JSONL
kb get team/ # Browse entities
kb datalog rules.dl # Run Datalog rules and queriesEach fact connects multiple typed entities as a hyperedge. The store indexes every entity for fast lookups from any direction.
{"edges": [["team", "Wolves"], ["team", "Rockets"], ["rel", "won"]], "source": "league"}Use @map directives in .dl files to bridge hypergraph facts into Datalog predicates:
@map plays_for(P, T) = [rel:plays_for, player:P, team:T].
@map won(W, L) = [rel:won, team:W, team:L].kb uses Datalog — a declarative language where you define rules that derive new facts from existing ones. The engine applies rules repeatedly until no new facts are derived (fixpoint).
won("Wolves", "Rockets").
won("Bears", "Wolves").
% Direct rule
dominates(A, B) :- won(A, B).
% Recursive — finds the full chain no matter how deep
dominates(A, C) :- won(A, B), dominates(B, C).
?- dominates("Bears", X). % X = Wolves, X = RocketsUse _ to ignore a position. Each _ is independent.
has_roster(T) :- plays_for(_, T). % any team with at least one playernot filters out bindings where a fact exists. Variables in negated atoms must appear in a positive atom in the same rule (safety requirement). Rules with negation are automatically stratified.
loser(T) :- won(_, T).
unbeaten(T) :- team(T), not loser(T).=, !=, <, >, <=, >= filter bindings without generating new facts. Both sides must be bound by a positive atom. Numeric when both values parse as integers, lexicographic otherwise.
high_scorer(P) :- points(P, Pts), Pts >= "20".
mid_range(P) :- points(P, Pts), Pts >= "10", Pts < "20".
rivals(A, B) :- won(A, B), A != B.A complete example combining all features — see tests/fixtures/demo.dl:
% Facts
plays_for("Alice", "Rockets"). plays_for("Carol", "Wolves").
points("Alice", "28"). points("Carol", "35").
won("Wolves", "Rockets"). won("Bears", "Wolves").
% Recursive dominance
dominates(A, B) :- won(A, B).
dominates(A, C) :- won(A, B), dominates(B, C).
% Negation: teams with no losses
loser(T) :- won(_, T).
unbeaten(T) :- team(T), not loser(T).
% Comparisons + recursion: high scorers on dominant teams
high_on_team(P, T) :- points(P, Pts), Pts >= "20", plays_for(P, T).
top_threat(P) :- high_on_team(P, T), dominates(T, "Rockets").
?- top_threat(P). % Carol (Wolves dominate Rockets, Carol scored 35)Rules are evaluated using a bitmap-based semi-naive engine. Relations are stored as roaring bitmap sets, and joins are computed via bitmap intersection. Stratification handles negation by partitioning rules into layers evaluated in dependency order.
LMDB provides memory-mapped, concurrent-read storage. Data persists in the .kb/ directory.
MIT