2026-10-26 21:42:04 +00:00

LoreGraph

A manga/manhwa chapter tracker whose wiki grows with the reader.

Public wikis assume you have finished the series. LoreGraph inverts that: it builds a temporal knowledge graph where every fact carries the chapter it became true, and every read path is filtered by your own progress. The same URL renders different content for a reader at chapter 12 and one at chapter 400.

Fully self-hosted. No paid external APIs — local LLM inference via Ollama, CV via magiv2, storage in PostgreSQL.

Status

Phases 0-3 are built: de-risking spikes, the data plane and spoiler guard, the CV ingestion pipeline, and LLM triple extraction with temporal graph injection. What remains before Phase 3 can be called finished is measurement, not code — the gold set is too small to move the precision and recall gates honestly.

Document What it covers
HANDOFF.md Where the work stands right now, what is open, what is next
CLAUDE.md Architecture, invariants, constraints, and commands
spikes/_results/PHASE_0_SUMMARY.md What the four spikes measured and which assumptions they overturned
PHASE_1_STATUS.md What the spoiler guard does and what remains
PHASE_2_STATUS.md The ingestion and CV pipeline
PHASE_3_STATUS.md Extraction, injection, and the open quality gates
DEPLOYMENT.md Bringing up a production database from nothing

Quick start

docker compose up -d                 # Postgres 16 + AGE + pgvector, and Ollama
python -m venv .venv && .venv/Scripts/python -m pip install -e ".[dev]"
.venv/Scripts/python -m alembic upgrade head
.venv/Scripts/python -m pytest -q --cov

Run everything through .venv/Scripts/python; the interpreter on PATH does not have the package installed. Postgres listens on 5442. See CLAUDE.md for the full command reference.

The core guarantee

The spoiler cutoff is enforced by PostgreSQL row-level security, not by application code. lore.cutoff(series_id) derives it from reading_progress inside the database and returns LEAST(requested_view, actual_progress) — a caller can only ever lower it. A query that forgets its WHERE clause returns nothing rather than everything.

The relation vocabulary is per-series

Which relationships a story can assert is data, not a constant. A shared core of about twenty relations covers what holds across genres — kinship, alliance, membership, location. Each series then extends it with its own, proposed by the model from that series' opening chapters and approved by a person during bootstrap.

This matters because the vocabulary is enforced by constrained decoding: a model facing a relationship it has no name for has exactly one escape, NO_RELATION, which is indistinguishable from "this line asserts nothing". A fixed list would silently give a cooking series or a sports series a thin wiki and never say why.

The proposals come from the pipeline itself: a vocabulary stage runs between chunking and extraction, reads the series' opening chapters, and asks the model what kinds of relationship they assert that the core set cannot express. It is the one stage that runs during the bootstrap hold rather than after it — gating it would deadlock, since release requires no outstanding proposals and proposals only exist once it has run.

The gold set is the one place the vocabulary is deliberately not live. Each gold world is a series with its own pinned vocabulary, so a prompt change is measured against a fixed enum — but a per-series one, carrying a predicate of its own, so the dynamic path is exercised rather than left untested. Onboarding a real series cannot move the gold numbers.

An unreviewed proposal is invisible to the extractor, and bootstrap will not release while any remain. Approving one creates its edge label in the series graph and its spoiler policy in the same step, so a relation type can never exist without a guard behind it.

GET    /series/{id}/vocabulary      # approved relations, and any awaiting review
POST   /series/{id}/vocabulary      # propose one by hand
POST   /vocabulary/{id}/approve     # approve: creates the edge label and its policy
DELETE /vocabulary/{id}             # discard a proposal (approved ones are permanent)

The gold set

Extraction quality is measured against hand-labelled ground truth. Labelling is interactive and writes straight to Postgres:

.venv/Scripts/python -m loregraph.label --propose   # review, with model suggestions
.venv/Scripts/python -m loregraph.goldset export    # then commit goldset_labels.json
.venv/Scripts/python -m loregraph.eval --verbose    # score, and record the run

goldset_labels.json is the only copy of that work outside a Postgres volume, so export after a session. The labels are judgements a person made; nothing in the pipeline writes them, and the suite fails if a test run ends with fewer of them than it started with.

Chapter images are never committed to this repository.

S
Description
No description provided
Readme
865 KiB
0 Stars 1 Watchers 0 Forks
Languages
Python 91.1%
PLpgSQL 8.4%
Dockerfile 0.4%