Biblia & Graphia — a study of different versions of the Holy Bible using a single-file DuckDB graph to map versions and verses of the Bible together with locations and regions.
This is a refactor of the original
TypeDB version of Bibliagraphia. The
graph is now a single-file DuckDB database (one nodes table, one edges
table) queried with parameterized recursive SQL, served by a FastAPI API
with a React (Vite) frontend.
The original TypeDB loader (bible_loader.py) and schema
(bible_schema.tql) are kept in the repo for reference.
The Bible exists in many translations, and most tools treat it as a flat book: search by keyword, look up by reference. Bibliagraphia instead stores open-licensed translations (KJV, Latin Vulgate, Douay–Rheims, Lexham English Bible, Statistical Restoration Greek New Testament and hopefully more...) in a single queryable graph:
- verses connect to their books and versions
- every place mentioned connects to a geocoded location and its region. The result is a non-linear way to explore the scriptures: walk the graph around a verse, compare one verse side by side across translations, trace the shortest path from a book to a region, or plot every place a book mentions on a Roman Empire–era map.
Bibliagraphia has been selected as one of the fifteen project briefs for BUILD 26, the FaithTech UK hackathon, where the focus will be exploring how the graph could power a personal knowledge graph — mapping reflections onto verses — and finding new ways to visualise the connections. Thanks to the BUILD team for including an independent project among the briefs.
The graph database maps Bible verses with:
- Multiple translations (Vulgate, Douay-Rheims, King James)
- Geographical location references
- Regional information
- Full book and chapter structure
The database is built from the canonical JSON files in /data:
books.json— Bible books with translation names (73 books)versions.json— Bible version information (VUL, DRB, KJV, LEB, SRG)verses.json— Complete verse text (141,788 verses across 5 versions; SRG is New Testament only)location_regions.json— Geographical locations with coordinates (7,460 mentions)regions.json— Regional descriptions and keywords (36 regions)figures.json— Biblical figures (243), each with a STEP Bible TIPNR id
Figure → verse links (figure_in_verse edges) use two datasets from
STEP Bible: TIPNR (every verse each person
appears in) and TVTMS (verse-numbering differences between Bible traditions).
STEP Bible asks that their data isn't redistributed, so it isn't stored in
this repo: scripts/build_db.py downloads it from their GitHub on the first
build and caches it in data/.stepbible/ (gitignored). Without internet
access the build still works, just without figure edges.
The graph is heterogeneous, stored in two tables:
| column | purpose |
|---|---|
id |
synthetic key, e.g. book:GEN, verse:DRB:GEN:1:1, region:Syria |
label |
node type: version / book / verse / region / location |
name |
human-readable name (for search & display) |
version_code, book_code, chapter, verse_number |
pulled-out query columns for verses/locations |
attrs |
JSON blob of all other type-specific attributes |
| label | from -> to | meaning |
|---|---|---|
verse_in_book |
book -> verse | a book contains a verse |
verse_in_version |
version -> verse | a version contains a verse |
location_in_verse |
verse -> location | a verse mentions a location |
location_in_region |
region -> location | a region contains a location |
figure_in_verse |
verse -> figure | a verse mentions a figure |
figure_relative_of |
figure -> figure | kinship from TIPNR genealogy: father/mother/parent edges run parent -> child, sibling/partner edges stored once per pair; attrs.relationship names the kind |
Figures that merely share a verse (e.g. Peter and Moses both in Luke 9:33)
are not given an edge — the link is a path, person → verse → person, queried
by joining the verse end of figure_in_verse (see sql/queries.sql).
There are deliberately no self-referencing foreign keys: edges are only created between nodes actually present (referential integrity), which keeps the graph clean.
Python 3.9 or newer is supported.
# Either install from pyproject (creates the `bibliagraphia` package + entrypoints):
uv venv venv && source venv/bin/activate && uv pip install -e ".[dev]"
# …or just the runtime deps:
# uv pip install -r requirements.txt
# 1. Build the graph from the JSON sources (a few seconds):
venv/bin/bibliagraphia-build # -> data/bible.db
# (equivalent to: venv/bin/python scripts/build_db.py)
# 2. Run the API (port 8000):
venv/bin/uvicorn app.api:app --reload
# 3. Run the React frontend (see "Vite + React frontend" below) — or the
# full two-container stack with `docker compose up -d --build`, which
# serves the app at http://localhost:8080.The API auto-builds data/bible.db from the JSON on first run if it's
missing, so step 1 is optional for local play.
Use the explicit development override to run Vite instead of nginx at
http://127.0.0.1:8080 (Docker Compose v2+):
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --buildFrontend source, public assets, HTML and Vite configuration are bind-mounted.
React edits update the browser automatically using Hot Module Replacement;
Vite configuration changes restart the dev server automatically. Polling
supports file changes through Docker Desktop mounts. Dependencies stay inside
the image, separate from host node_modules. After changing either dependency
manifest, rebuild only the frontend:
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build --no-deps frontendThe API container and its image-built Bible database are unchanged. Vite
proxies API requests to http://api:8000 in this mode; normal host development
still uses http://127.0.0.1:8000. Python edits are not hot-reloaded.
Switch back to the production-style nginx frontend with:
docker compose -f docker-compose.yml up -d --build --no-deps frontendRailway continues building the final nginx stage of the same Dockerfile. It does not use the development override, source mounts or file watchers.
The standalone React frontend lives in frontend/ and is the sole UI for
the API. Start the API in one terminal, then run:
cd frontend
npm install
npm run devThe frontend uses Vite, React, and Tailwind CSS. Tailwind utility classes can be
used directly in the JSX components under frontend/src/.
frontend/src/styles.css is the import entry point,
not a component stylesheet. Styles are organised under
frontend/src/styles/:
tokens.cssmaps light/dark theme variables to Tailwind colours and fonts. The underlying CSS variables remain available to the graph canvas and Leaflet.base.csscontains document defaults and keyboard focus styling.shared.csscontains reusable panel, form, button and result-row recipes.landing.css,header.css,explore.css,graph.css,reader.cssandmap.cssown their respective feature styles.widgets.cssowns the Explore dashboard and dedicated discovery-tool layouts.leaflet.csscontains third-party map and popup overrides. These intentionally stay outside cascade layers so they can override Leaflet's unlayered stylesheet. Leaflet's core stylesheet is imported centrally bystyles.cssbefore these overrides, so standalone and reader maps never depend on another widget loading.motion.csscontains animations and reduced-motion rules.
Prefer Tailwind utilities directly in JSX for simple elements. Use @apply
recipes in the relevant feature's @layer components for shared selectors,
responsive layouts and complex component states. Use theme utilities such as
bg-panel, text-ink, text-muted and border-line rather than duplicating
theme values. Keep semantic class hooks used by JavaScript and tests.
Utilities in one @apply may be reordered by Tailwind. Keep overlapping
shorthand and longhand declarations in separate @apply statements when their
order matters (for example font followed by font-size).
The header logo links to the home page. Open the settings menu (three-line
icon) to switch between light and dark mode; the choice is saved across pages.
Main navigation is ordered Home, Read, Map, Relationships, Explore, MCP, About, with
decorative icons alongside each label. It scrolls horizontally on small screens.
Home offers a Try it with… suggestion rotating through Ruth, Genesis, Jonah,
Matthew 7, Exodus, Luke and Joshua 3:16 on each homepage visit. The previous suggestion is saved
in browser storage to avoid consecutive repeats. Links open chapter 1, Matthew
chapter 7, or Joshua chapter 3 with verse 16 selected, in KJV and DRB side by side,
plus links to About, AI assistant setup and GitHub. After a chapter with text
loads successfully, its book, chapter, translation and comparison choices are
saved locally in the browser. The home Read card then offers Continue reading;
first-time visitors see Start reading. No account is needed.
The reader's Book picker shows available books grouped by testament. Type a
book name or code to filter immediately, then click a result or use arrow keys
and Enter to select it. Escape dismisses the picker without changing the passage.
Selecting a book opens its first available chapter and preserves translations.
Open /about for the team (Ally, Jonah, Elle, Sidd and Jonathon), playful
team icons, canonical data sources and project/map/software acknowledgements.
Dataset counts are explicitly labelled as the README baseline, not live totals.
Open /explore for a fixed dashboard of live preview widgets. Book suggestions,
Genesis 1:1 translations, a Genesis relationship graph, mapped place mentions
and a Genesis-to-Syria path load automatically. Each widget links to its full
tool and displays loading, empty and error states; failed previews can be retried.
/explore/search opens the search tool with Genesis results already loaded.
Search includes biblical figures with an About button for their details
and an exact-item link to Relationships.
/explore/connections asks "What's connected?" and "How are these connected?",
initially showing Genesis passages and its connection to Syria. Searchable
pickers offer three suggestions per type and use exact IDs, so selecting a
passage or place mention preserves its identity. The first tool accepts books,
regions and translations; the second connects any two items. Results explain
recorded links in plain English, respecting their direction, and open exact
items in Relationships. Changing a choice clears outdated results.
The /traverse and /path APIs accept exact IDs as well as names and codes.
Read/compare, graph and map widgets open /read, /relationships and /map.
Place preview links use /map?place=... to select a matching place or alias.
Open /relationships for a full-viewport relationship graph. It starts at
Genesis by default. The centre selector and depth control sit together at the
graph's top left. Click the selector to search across books, passages,
places, translations and regions, with up to three suggestions per type.
Choosing a suggestion automatically sets its type and exact node ID.
The three-dot menu includes Show names to display all graph labels.
This preference is saved in the browser; hover and zoom labels still work when
the toggle is off. Graph labels use rounded, theme-aware backgrounds, with a
node-coloured border for the centre and hovered node.
The How far to explore selector offers 1–20 connection steps and defaults to 3.
Each step follows one recorded connection. The selected node, type and depth are
saved in browser storage and restored when returning to Relationships.
The URL includes depth as well as node identity for refreshes and sharing;
explicit URL values take priority over saved selections. The 200-node cap still
applies, so larger depths may show a truncated neighbourhood.
Relationships uses passage-first, branch-limited expansion: each branch initially
explores up to 10 verse nodes, 5 place nodes and 5 nodes of each other type.
The sidebar lists hidden direct connections; Show more connections widens
the selected centre's branch while retaining the total 200-node budget.
Select another node to explore its branch. Hidden counts are distinct neighbours
not loaded in this graph, not a total count of all paths or passages.
Verse translations and repeated place mentions remain separate exact-ID nodes.
The node-type key at the bottom right includes checkboxes to hide or show each
type and its incident edges. Filtering only affects the view: loaded data,
layout positions and sidebar connections remain intact, and hidden nodes cannot
be clicked on the canvas. Show whole view fits visible nodes. Selecting a new
centre reveals its type if hidden. The bottom-left summary reports visible versus loaded items.
Choose a centre and depth to explore another neighbourhood. Selecting a node
redraws immediately; depth changes redraw after a short pause. No Draw graph
button is needed on this page. Node
symbols identify books, verses, translations, places and regions. Drag to pan,
scroll to zoom, click a node to explore its neighbours, or use the node picker
inside the three-dot options control beside the depth selector
for keyboard navigation. Show whole view and fullscreen controls sit at the top
right; the node-type key sits at the bottom right. Show whole view restores the view.
Node/connection counts and truncation status sit at the bottom left.
The graph includes people, labelled People in filters and Person in details.
Kinship edges between figures are drawn dashed
in the figure colour (keyed Family relationships in the node-type key), and the sidebar
names the recorded kind — Father of, Mother of, Child of, Sibling of, Partner of.
Underlined places in the reader open details with a Related passages and places
preview showing the place and up to six directly connected nodes. Its See more
control opens the full graph centred on that exact place mention. Selecting a graph node preserves its
identity, including passages sharing a book name. Verse details show passage
text, a Read passage link, and links to directly connected places and their
maps. Place details link back to the reader and map. Graph centres are saved in
the Relationships URL for reloads and sharing.
Selected node details appear beside the graph in a right-hand sidebar, stacking
below the graph on narrow screens.
The sidebar groups directly connected nodes into collapsible sections by type.
Each entry describes its relationship to the selected node and opens that exact
node when clicked. Counts describe the loaded graph, not the entire database;
capped graphs show a reminder that additional connections may exist.
The bottom-left graph summary includes the live visible/loaded node count.
Map markers are circles across the map, explorer and reader previews.
Open /read in the Vite app to read a Bible book. Choose a book, chapter,
translation, or jump directly to a verse; previous/next controls move between
chapters.
Book, chapter, verse and translation controls sit between Previous and Next
in one navigation bar, with grouped rows on mobile. Use the + button
(Compare translations) beside the translation selector
to compare versions side by side. In comparison mode, each column has its own
translation selector; selecting an already displayed version swaps columns
without duplicates. Shared book, chapter and verse controls keep columns in sync;
verses align by number (numbering can differ between translations). Unavailable
text is labelled rather than replaced with another passage. Remove columns
with their × buttons; comparison selections are preserved in the reader URL.
On narrow screens, scroll the comparison horizontally.
Underlined place mentions in every column open place
details with links to the graph and map. Open /map to browse all mappable
places; select a marker to read the passages that mention it.
Every item in the reader's Related passages and places preview links to Relationships
centred on that exact item, retaining passage translation and place-mention IDs.
People named in a verse have a pink, solid-underlined button opening person
details beside the passage. The drawer shows the recorded description, a related
items preview and Family relationships (kinship) when available. A separate
/reader/person-relations lookup returns all direct relatives, independently of
the graph preview's node cap, with stored relationship kinds and directions.
Select a relative to open their details in the same drawer. An explicit
empty state appears when no family relationships are recorded; no
relationships are invented. Places retain their dotted highlight and drawer.
Selecting a verse number opens passage text and related items in the same
details drawer, retaining the exact translation of that comparison column.
Preview items and Explore all connections link to Relationships.
Reader previews include at least one item of every directly connected type
available, then fill remaining spaces up to six. Balanced graph loading prevents
passages from crowding out other types. The + See more control and each item's
plus-marked link open Relationships centred on its exact ID. The full view retains
its normal limits and Show more connections controls; missing types are not invented.
People highlights use recorded STEP Bible verse links and canonical names or
explicit spelling aliases, never general search keywords. Only names present
in the people dataset are highlighted; pronouns are not. Each comparison
column uses its own translation's verse links.
Use the map's search box to find places by name or alternative name. Choose an
autocomplete suggestion (or use the arrow keys and Enter) to zoom to that place
and open its passages.
The expand control on a place's preview map smoothly grows it into the full
map, preserving its centre and zoom. Browser Back restores the passage and
place sidebar. The animation uses the View Transitions API; unsupported
browsers and reduced-motion preferences retain normal page navigation.
Open the Vite URL shown in the terminal (normally http://localhost:5173).
Vite proxies API requests to http://127.0.0.1:8000. For a deployed frontend
or an API on another host, set VITE_API_BASE_URL before building, for example
VITE_API_BASE_URL=https://api.example.org npm run build.
All endpoints use parameterized SQL (no string interpolation), so there is no SQL-injection surface.
| method & path | body / params | returns |
|---|---|---|
GET /health |
— | {status, db, empty} |
GET /search?q=&label= |
q prefix, optional label filter |
autocomplete node list |
POST /traverse |
{start_node, label, edge} |
recursive descendants along edge + their edges (kinship edges carry their kind in attrs) |
POST /path |
{source, source_label, target, target_label} |
shortest path (BFS) between two nodes, edges carry attrs |
GET /graph?node=&label=&node_id= |
node name and label; optional exact node ID | nearby nodes and relationship edges; kinship edges carry their kind in attrs |
GET /map/points?region=&book=&limit= |
a region or book code | geocoded location mentions for selected scope |
GET /map/places |
— | unique mappable places with repeated mentions grouped |
GET /verse?book_code=&chapter=&verse_number= |
verse reference | the same verse across all versions |
GET /reader/catalog?version_code= |
optional version code (defaults to KJV) |
books, translations, and available chapters |
GET /chapter?book_code=&chapter=&version_code= |
passage reference | ordered verse text and linked location nodes |
GET /place/relations?location_id= |
location mention node ID | translations and other references for the place |
GET /explain?book_code=&chapter=&verse_number=&version_code= |
verse reference | graph context for the verse + an AI chat explanation (needs MISTRAL_API_KEY) |
Examples:
# all locations in Syria
curl -XPOST localhost:8000/traverse -H 'content-type: application/json' \
-d '{"start_node":"Syria","label":"region","edge":"location_in_region"}'
# cross-version John 3:16
curl "localhost:8000/verse?book_code=JOH&chapter=3&verse_number=16"
# autocomplete
curl "localhost:8000/search?q=Jer&label=location"
# explain a verse (set MISTRAL_API_KEY on the server first)
curl "localhost:8000/explain?book_code=JOH&chapter=3&verse_number=16"GET /explain gathers the graph's own facts for a verse — the verse in
every available translation, the verses immediately above and below for
reading context, the places the graph links to it, and those places'
region descriptions — then asks the Mistral model (OpenAI-compatible
API) for a chat reply explaining the passage, the place, and the persons
involved. The model only ever sees
facts the backend gathered; it is instructed never to invent places the
graph does not link. If the model plans before it writes, the backend
strips its planning block, so the reply
shown to the reader is the explanation itself.
The reply carries the evidence with it:
{
"reference": {"book_code": "JOH", "book_name": "...", "chapter": 3, "verse_number": 16, "version": "KJV"},
"verse": {"text": "...", "versions": [{"code": "KJV", "text": "..."}, {"code": "DRB", "text": "..."}]},
"context": {"previous": {"chapter": 3, "verse_number": 15, "text": "..."}, "next": {"...": "..."}},
"locations": [{"name": "Damascus", "region": "Syria", "latitude": 33.52, "longitude": 36.31}],
"regions": [{"name": "Syria", "keywords": "...", "description": "..."}],
"explanation": "the model's chat reply"
}Configuration (environment variables on the API server):
| variable | default | purpose |
|---|---|---|
MISTRAL_API_KEY |
— (required) | bearer token for the Mistral API |
MISTRAL_API_BASE |
https://api.mistral.ai/v1 |
OpenAI-compatible base URL |
MISTRAL_MODEL |
mistral-large-latest |
model name |
MISTRAL_TIMEOUT |
60 |
request timeout (seconds) |
MISTRAL_MAX_TOKENS |
6000 |
token budget per model reply |
Without a key the endpoint still gathers the graph context but returns
{"error": "MISTRAL_API_KEY is not set ..."}.
The API also speaks the Model Context Protocol
(streamable HTTP) at /mcp — same process, same port, no extra
infrastructure (FastMCP mounted into the FastAPI
app). Any MCP-capable LLM client can walk the Bible graph directly.
The tools wrap the same route handlers as the REST API — one implementation, two interfaces:
| tool | wraps | purpose |
|---|---|---|
search_nodes |
GET /search |
autocomplete / resolve names to codes |
get_verse |
GET /verse |
one verse across all versions |
read_chapter |
GET /chapter |
ordered chapter text with location links |
reader_catalog |
GET /reader/catalog |
books, versions, available chapters |
traverse_graph |
POST /traverse |
recursive walk along one edge type |
find_path |
POST /path |
shortest path between two nodes |
graph_neighborhood |
GET /graph |
ego-graph around a node |
map_locations |
GET /map |
geocoded mentions (lat/lng + verse text) |
explain_verse |
GET /explain |
verse explanation from the graph context via the Mistral model |
Open MCP in the navigation (/connect-mcp) for connection URLs, copyable
VS Code and Claude Code configurations, troubleshooting and example prompts.
The guide uses https://bibliagraphia.com/mcp in every client example, with
no server selector.
Both nginx and the local Vite server proxy /mcp to the API. nginx disables
response buffering and keeps streaming sessions open; deploy the updated
frontend configuration before using the public endpoint.
/mcp remains the protocol endpoint on the API, not a frontend page.
Claude Code (.mcp.json):
{
"mcpServers": {
"bibliagraphia": {
"type": "http",
"url": "https://api.bibliographia.com/mcp"
}
}
}VS Code uses .vscode/mcp.json with a top-level servers object instead of
mcpServers; each server has "type": "http" and the same URL. Other clients
should follow their own remote-server configuration instructions.
Any streamable-HTTP client uses the same URL. Locally:
http://localhost:8000/mcp.
FastMCP ships a client that performs the whole streamable-HTTP dance
automatically — initialize, session-id capture, notifications/initialized,
session echo. scripts/mcp_client.py wraps it:
# list the tools the server exposes
python scripts/mcp_client.py tools
# call one tool (args as JSON)
python scripts/mcp_client.py call get_verse \
--args '{"book_code": "JOH", "chapter": 3, "verse_number": 16}'
# ad-hoc REPL: one tool call per line, q to quit
python scripts/mcp_client.py interactiveEndpoint defaults to http://localhost:8000/mcp; override with --url or the
BIBLIAGRAPHIA_MCP_URL environment variable (e.g.
https://api.bibliographia.com/mcp).
For debugging the transport itself, the manual version:
Streamable HTTP is session-based: initialize returns an mcp-session-id
response header that later requests echo back.
API=https://api.bibliographia.com/mcp
# 1. initialize → capture the session id from the mcp-session-id header
SESSION=$(curl -s -D - -o /dev/null -X POST $API \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}' \
| awk 'tolower($1)=="mcp-session-id:" {gsub("\r","",$2); print $2}')
# 2. required notification
curl -s -X POST $API -H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-H "mcp-session-id: $SESSION" \
-d '{"jsonrpc":"2.0","method":"notifications/initialized"}'
# 3. list tools
curl -s -X POST $API -H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-H "mcp-session-id: $SESSION" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
# 4. call one: cross-version John 3:16
curl -s -X POST $API -H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-H "mcp-session-id: $SESSION" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_verse","arguments":{"book_code":"JOH","chapter":3,"verse_number":16}}}'- Visiting
/mcpin a browser returnsBad Request: Missing session ID— that is the transport enforcing the handshake, not an error. Use an MCP client. - If the API sits behind the Cloudflare proxy, grey-cloud (DNS only) the record: Cloudflare buffers SSE streams, which stalls MCP responses.
Bibliagraphia/
├── data/ # canonical JSON sources (+ generated bible.db)
│ ├── books.json versions.json verses.json
│ ├── location_regions.json regions.json figures.json
│ ├── .stepbible/ # STEP Bible download cache (gitignored)
│ └── bible.db # generated — do not edit
├── scripts/
│ ├── build_db.py # JSON -> data/bible.db (idempotent, bulk-loaded)
│ ├── stepbible.py # STEP Bible TIPNR/TVTMS: figure refs + verse numbering
│ └── mcp_client.py # MCP client — handshake handled for you (see “MCP server”)
├── sql/
│ ├── init_duckdb.sql # schema (reference)
│ ├── load_data.sql # load steps (reference)
│ └── queries.sql # recursive CTEs (reference)
├── app/
│ ├── api.py # FastAPI /search /traverse /path /verse /reader /chapter /health
│ └── mcp.py # FastMCP server mounted at /mcp (see “MCP server”)
├── frontend/ # Vite + React frontend (the UI; built + served by frontend/Dockerfile)
├── tests/
│ └── test_graph.py # build + counts + traverse + path + verse
├── bible_loader.py # legacy TypeDB loader (kept for reference)
├── bible_schema.tql # legacy TypeDB schema (kept for reference)
├── stopwords.py # legacy NLP helper
├── requirements.txt # Dependencies
└── README.md # README
This refactor replaces the TypeDB server with a single DuckDB file + recursive SQL. The Bible graph is ~110k nodes and fits trivially in RAM, so a single in-process database is the right shape here — no server, no sharding, no network.
Key choices:
- Two tables,
nodes+edges, with alabelon each (the Bible graph is heterogeneous, so a single sharednodestable needs a type column). scripts/build_db.py— one idempotent script that reads the flat JSON sources and bulk-builds the DB withINSERT ... SELECTviaread_json_auto.- Parameterized recursive SQL for traversal (
WITH RECURSIVE), and a bounded BFS for shortest path. The edges table is heterogeneous, so traversal filters onlabel. app/api.py— FastAPI exposing/traverse,/path,/search,/health,/verse,/reader/catalogand/chapterendpoints. The UI is the separate React frontend infrontend/(nginx proxies its API calls to this service).- No self-referencing foreign keys — edges only ever connect nodes we have (referential integrity), which keeps partial views clean.
The win over the original TypeDB setup: no database server to run, a ~3-second build, parameterized queries, and a real API + frontend.
/pathfinds a shortest path through the heterogeneous graph; because locations link to both verses and regions, BFS may hop through topically-unrelated nodes that share a region. That is a property of the graph, not a bug — and exactly the kind of walk the model enables.data/bible.dbis generated; rebuild it withscripts/build_db.py.
venv/bin/python -m pytest # 5 tests, all green
venv/bin/ruff check . # lint (legacy TypeDB files excluded)Tool config (pytest paths, ruff rules) lives in pyproject.toml.
The API startup compatibility check needs no source dataset:
python -m unittest discover -s tests -p test_api_startup.pyGNU General Public License v3.0
Figure verse references and verse-numbering mappings come from STEP Bible (TIPNR and TVTMS datasets, Tyndale House Cambridge), used under CC BY 4.0. Changes: matched to Bibliagraphia figures (some limited to Genesis, where TIPNR's record also covers a tribe) and converted to each version's verse numbering. The data itself is not redistributed here; it is downloaded at build time.
Ave Christus Rex
✝️

