Reviewed-on: #1
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.