Brings the docs into sync with what shipped in
slice 6 (712→761 tests, 7 sub-slices):
- Setting↔Plane membership is encoded as a
``setting_id`` field on the Plane node, not as a
HAS_PLANE edge. The pure-Cypher pattern in the
design is the *intent*; the engine stores it
differently for the 1-to-many reverse-lookup.
Updated 01-ontology.md, 11-extensibility.md,
14-examples.md, 17-planes.md accordingly.
- EXISTS_IN (entity → Setting) is the timeless
type-assertion; time-bounded planar location is
LOCATED_IN_PLANE (entity → Plane) with valid_from
/valid_until. Updated the edge-type table, the
Cypher examples, and the migration story.
- 17-planes.md: clarified "EXISTS_IN is many-to-many
(setting level, not plane level)"; the legacy v1.1
EXISTS_IN (entity → plane) is folded into
LOCATED_IN_PLANE for the time-bounded case and
removed from the timeless one.
- ADR 0013 — the v1.2 plane-model migration story.
Documents the 7 locked decisions (Setting/Plane
first-class, setting_id field encoding, the
GraphBackend Protocol extensions, the read-tool
setting filter, the 4 plane-relation edge types,
the migration's idempotency), what was rejected
and why, and the consequences for the Neo4j
parity follow-up.
Co-Authored-By: Claude <noreply@anthropic.com>
18 KiB
01 — Ontology
The world is modeled as a typed, temporal, source-attributed property graph. This document is the contract: every node label, every edge type, every property that the engine reasons over. If a fact isn't expressible in this schema, either the schema is wrong or the fact doesn't belong in the engine.
Design principles
- Everything is a node that should be. If we want to reason about it, name it, and give it a label. Even abstract concepts (titles, languages, deities) get nodes.
- Edges have semantics. A relation is not "linked to" — it is
RULED,PARENT_OF,SPEAKS,OCCURRED_DURING. Every edge has a typed, time-bounded meaning. - Time is structural. Not a property string. Temporal validity is encoded as edge properties (
valid_from,valid_until) and node connections (:OCCURRED_DURING,:EXISTED_DURING). - Sources are first-class. Every claim traces back to a
LoreSource. The LLM is told which document said what. - Sparse properties beat dense ones. Don't stuff a node with 30 fields. If we need to model something complex, model it as a node.
- Names are unique within type, not globally. "Vyr" is the name of a Faction; "Vyr" can also be a given name of a Person. Uniqueness constraints are scoped per-label.
Node labels
Base types (Lore Engine originals on Cognee's DataPoint)
Cognee provides the
DataPointbase class and its own chunking/embedding — that is what's genuinely inherited. The labels below are Lore Engine types built on top ofDataPoint, not Cognee primitives. In particular,Contradiction(and the other consistency nodes) are built from scratch in slice 2 — Cognee ships no contradiction machinery.Message/Topicare legacy ingestion concepts retained for dialogue logs.
| Label | Purpose | Key properties |
|---|---|---|
LoreSource |
A document ingested into the system. Replaces the flat LoreDocument concept; adds source_type (prose / timeline / family_tree / gazetteer / bestiary / dialogue). |
id, title, source_type, uploaded_at, author, confidence |
LoreChunk |
Vector-embedded chunk of a LoreSource. |
id, text, embedding, source_id |
Message |
Free-text event (Discord, in-fiction dialogue log, etc.). | id, content, author, timestamp |
Chunk |
Vector-embedded chunk of a Message. |
id, text, embedding |
Contradiction |
Flagged conflict between sources. | subject, predicate, claim_a, claim_b, doc_a, doc_b, flagged, detected_at |
Encounter |
In-world event witnessed by named characters (in-fiction time, not runtime). | id, title, type, summary, in_fiction_date |
Topic |
Loose categorical concept (used by message ingestion). | name, source |
New: World entities (lvl 0 — atomic things)
| Label | Purpose | Key properties | Example |
|---|---|---|---|
Person |
A named character, NPC, deity, or historical figure. | name, aliases[], birth, death, tier (commoner/noble/etc.), culture |
Aldric Raventhorne |
Faction |
A guild, kingdom, order, cult, party, house. | name, aliases[], founded, dissolved |
House Vyr |
Location |
A named place, region, landmark, realm. Not a node hierarchy — that is modeled via edges. | name, aliases[], coordinates (optional) |
Thornwall Keep, Valdorn Reach |
Creature |
A named or typed monster, beast, or non-person entity. | name, species, alignment |
The Pale Worm |
Item |
A named weapon, artifact, magical object, relic. | name, material, enchantments[] |
Sword of Eventide |
Title |
A held rank, office, or honorific. Decoupled from Person so the same title (Queen of Valdorn) can be traced across many holders. |
name, domain (Location or Faction) |
Queen of Valdorn |
Language |
A spoken/written language in the world. | name, script, speakers[] (Person, Faction) |
Old Valdorni |
Deity |
A named god, demigod, or divine being. | name, domain[], alignment, symbol |
Aelar the Patient |
Spell |
A named magical effect. Has a MagicSystem parent (see lvl 1). |
name, level, school |
Emberlance |
Material |
A specific substance with lore significance (orichalcum, soulglass, etc.). | name, rarity |
Soulglass |
Plane (v1.2) |
A first-class graph node representing a layer of existence (Material, Shadowfell, demiplane, Outer Plane, etc.). Every Plane belongs to exactly one Setting (via the setting_id field on the Plane; reverse lookup via planes_in_setting(setting_id)) and can have relations to other planes (REFLECTS, LAYER_OF, ADJACENT_TO, ACCESSIBLE_VIA). See 17-planes.md for the full model. |
id, name, setting_id, kind (material/reflection/transit/ethereal/outer/inner/demiplane/transcendent), summary, accessible, alignment_tendency, valid_from, valid_until |
eberron.material, mardonari.voldramir |
Setting (v1.2) |
A campaign/world scope that owns a tree of Plane nodes. The "top of the world" in the engine. |
id, name, kind, current_era, schema_version, created_at, summary, genres[], canonical_time |
eberron, mardonari, default (legacy alias) |
New: Player-vs-NPC separation (v1.1)
Per Kay's answer (Q3), NPCs and PCs (and the humans behind PCs) are tracked separately.
| Label | Purpose | Key properties |
|---|---|---|
NPC |
An in-fiction character controlled by the engine / world-builder. Wraps or references a Person node. |
id, person_id (Person), controlled_by (always "engine" or "world_builder"), personality_summary (free text), voice_id (TTS voice, optional) |
PC |
An in-fiction character controlled by a human player. | id, person_id (Person), controlled_by (always "human"), player_id (Human, see below) |
Human |
A real human, the player behind a PC. Tracks the player separately from the in-fiction character. Optional — only created if the world-builder wants to associate the two. | id, name, discord_id?, email?, joined_at, notes (world-builder's notes about the human, e.g. "prefers intrigue plots") |
The relationships:
(:Human) -[:PLAYS]-> (:PC) -[:IS_PERSON]-> (:Person)
(:NPC) -[:IS_PERSON]-> (:Person)
This means:
- The in-fiction
Personnode is the canonical entity. NPCs and PCs both reference it. - NPC tier scoping (existing v1) works on the
Personnode.query_as_npcresolves to thePersonvia theNPCwrapper. - The human player (with their preferences, history, etc.) is a separate node. The world-builder can ask "what does this human player like?" without conflating it with "what is the in-fiction character like?"
- For purely-NPC worlds (no PCs), no
HumanorPCnodes are created. TheNPCandPersonnodes are sufficient.
This is the answer to "associate the true human and some details about them where possible." The world-builder can attach as much or as little to the Human node as they want, and the in-fiction layer stays clean.
New: Polymorphic domain entities (v1.1)
Per the extensibility question, the engine adds one new node label and one new edge label as a polymorphic substrate for anything the world-builder wants to model that isn't covered by the core types.
| Label | Purpose | Key properties |
|---|---|---|
DomainEntity |
A node representing a domain-specific concept (thieves-guild mission, war campaign, trade lot, ritual, spellbook, etc.). Defined by a TypeTemplate. |
id, type, name, setting_id, properties: map<string, any>, template_id, sources[], lore_verified |
Relation |
An edge between any two nodes (DomainEntity, Person, Faction, etc.) with typed properties. | id, from_id, to_id, type, properties: map<string, any>, valid_from, valid_until, sources[] |
TypeTemplate |
The schema for a domain type. Stored as data, hot-reloadable. | id, version, spec (parsed YAML), status (active/draft/deprecated) |
The full design is in 11-extensibility.md. The short version: a YAML file in ./templates/ defines a new domain type, the engine registers it, the world-builder writes instance YAMLs, and the LLM gets new tools automatically. No Go code change.
New: Structural & temporal (lvl 1 — how things relate through time)
| Label | Purpose | Key properties | Example |
|---|---|---|---|
Era |
A named span of in-fiction time. Hierarchical: an Era can contain sub-eras (e.g. Third Age → Age of Iron). |
name, start, end, parent_era |
Third Age |
Calendar |
The world's calendar system. Most worlds have one; some have many (regional calendars). | name, epoch_label, months[] |
Imperial Reckoning |
Lineage |
A family relationship group. Decoupled from Person so we can ask "who belongs to the Vyr bloodline?" without traversing PARENT_OF repeatedly. |
name, founding_ancestor (Person), cadet_branches[] |
House Vyr (bloodline) |
Culture |
A named cultural group (Valdorni highlanders, Ashen nomads, etc.). | name, language (Language), homeland (Location) |
Valdorni |
MagicSystem |
A coherent body of magic — the Weave, Runic Sorcery, Divine Miracles. Not an individual spell. | name, source (Deity, natural law, etc.), practitioner_types[] |
The Weave |
Region |
A named geographic grouping larger than a Location. Used for climate, political control, etc. | name, parent_region |
Northern Reaches |
New: System metadata
| Label | Purpose | Key properties |
|---|---|---|
OntologyRule |
A Cypher-defined consistency rule. Stored as data so the LLM can query "what rules apply here?" | id, cypher, description, severity (error / warn) |
Edge types
All edges are directed, typed, and (where meaningful) carry a temporal validity window.
Identity & structure
| Edge | From → To | Time-bound? | Notes |
|---|---|---|---|
ALIAS_OF |
Person/Faction/Location → Person/Faction/Location | no | The from is a name variant; the to is canonical. |
MEMBER_OF |
Person → Faction | yes | Period matters. Aldric was a member of the Order of the Silver Hand from 312 TA to 320 TA. |
PART_OF |
Location → Location / Region → Region | no | Hierarchy. Thornwall is part of Valdorn. |
CADET_BRANCH_OF |
Lineage → Lineage | no | Bloodline relationship. |
DESCENDED_FROM |
Person → Person | yes | Distant ancestry. |
PARENT_OF |
Person → Person | yes | Direct parentage. |
SIBLING_OF |
Person → Person | yes | Bi-directional semantically; stored as one edge per pair with a direction property or as two PARENT_OF traversals. |
SPOUSE_OF |
Person → Person | yes | Marriage. |
RULES |
Person/Faction → Location / Faction | yes | Governance or control. |
CLAIMS_TITLE |
Person → Title | yes | Holds a specific title. |
TITLE_OF |
Title → Faction/Location | no | The title's domain. |
Geography & residence
| Edge | From → To | Time-bound? | Notes |
|---|---|---|---|
LOCATED_IN |
Person/Faction/Item/Creature → Location | yes | Where they are now (or were at the time of the source). |
ORIGINATES_FROM |
Person/Faction → Location | no | Birthplace, founding place. |
CONTROLS |
Faction → Location | yes | Military/political control. |
NEAR |
Location → Location | no | Geographic proximity. |
Events & time
| Edge | From → To | Time-bound? | Notes |
|---|---|---|---|
OCCURRED_AT |
Event → Location | yes (start) | Where the event happened. |
OCCURRED_DURING |
Event → Era | yes | Which era the event belongs to. |
PARTICIPATED_IN |
Person/Faction → Event | yes | Direct involvement. |
WITNESSED |
Person → Encounter | yes | Lore Engine pattern for in-fiction events. |
CAUSED |
Event → Event | yes | Causal chain. "The Sundering caused the Long Winter." |
PRECEDED |
Event → Event | yes | Pure chronological. |
CONCURRENT_WITH |
Event → Event | yes | Overlap, not causation. |
EXISTED_DURING |
Person/Faction/Location/Item/Creature → Era | yes | When the thing existed. |
Knowledge & lore
| Edge | From → To | Time-bound? | Notes |
|---|---|---|---|
KNOWS |
Person → Person / Fact | yes | Acquaintance or knowing. |
MENTIONED_IN |
Entity → LoreSource | no | Reverse of FEATURES. |
FEATURES |
LoreSource → Entity | no | The document mentions this entity. |
SUPPORTS |
LoreSource → Claim | no | A document backing a contested claim. |
CONTRADICTS |
LoreSource → LoreSource | no | Two documents that disagree. |
Magic, culture, language
| Edge | From → To | Time-bound? | Notes |
|---|---|---|---|
PRACTICES |
Person/Faction → MagicSystem | yes | Actively uses this magic system. |
CASTS |
Person → Spell | yes | Specifically casts this named spell. |
PART_OF_SYSTEM |
Spell → MagicSystem | no | The system the spell belongs to. |
WORSHIPS |
Person/Faction → Deity | yes | Religious devotion. |
SPEAKS |
Person → Language | yes | Active fluency. |
BELONGS_TO |
Person → Culture | yes | Cultural identity (not necessarily citizenship). |
CULTURE_OF |
Culture → Location/Region | no | The culture's homeland. |
Planes (v1.2 — first-class graph nodes)
| Edge | From → To | Time-bound? | Notes |
|---|---|---|---|
EXISTS_IN |
any entity → Setting | no (timeless type-assertion) | "This entity type exists in this setting." The reverse lookup setting_entities(setting_id) resolves the membership. The slice 6.5 read-tool setting= filter consumes this edge. |
LOCATED_IN_PLANE |
any entity → Plane | yes | Where the entity was at time T. Reuses the time-bounded pattern. "Where was Roland in 430 TA?" |
REFLECTS |
Plane → Plane | no | Reflection-of-material relationship. Shadowfell REFLECTS Material. |
LAYER_OF |
Plane → Plane | no | Sub-layer relationship. Dis LAYER_OF Nine Hells. Also used for demiplane containment. Direction: the layer points at the parent. |
ADJACENT_TO |
Plane → Plane | no | Geometric neighbor. Material ↔ Ethereal. Symmetric. |
ACCESSIBLE_VIA |
Plane → Spell/Item/Location | no | Reachable by this mechanism. The "can I get there from here" relation. |
Note: Setting ↔ Plane membership is encoded as a
setting_idfield on thePlanenode, not as aHAS_PLANEedge. This is a simplification — every Plane has exactly one Setting, so a single field is sufficient. The reverse lookupplanes_in_setting(setting_id)is O(1) via theplanes_by_settingindex. The Cypher pattern from17-planes.mdis preserved; only the storage form changed.
Artifact & lineage
| Edge | From → To | Time-bound? | Notes |
|---|---|---|---|
POSSESSES |
Person/Faction → Item | yes | Currently holds. |
CREATED |
Person/Faction → Item | no | Crafted it. |
FORGED_FROM |
Item → Material | no | What it's made of. |
INHERITED_BY |
Item → Person | yes | Lineage of ownership. |
BURIED_AT |
Person → Location | no | Final resting place. |
FOUNDED |
Person/Faction → Faction/Location | no | Founded it. |
Properties on edges (the time model)
Edges that are time-bound carry a valid_from and valid_until, both encoded as {era}.{year} strings (see 02-time-model.md for the format). Open-ended means null:
(Aldric:Person {name: "Aldric Raventhorne"})
-[:RULED {valid_from: "3rd_age.year_340", valid_until: "3rd_age.year_352"}]->
(Valdorn:Location {name: "Valdorn"})
The MCP tool layer turns valid_from/valid_until into answers to "was X true at time T?" via a single indexable predicate, see 02-time-model.md.
Properties on nodes (shared)
{
// Identity
name: "Aldric Raventhorne",
aliases: ["The Crow", "Lord Aldric"],
// Source attribution (every node, regardless of label)
sources: ["chronicles-vyr.md", "voice-of-the-keep.md"],
source_confidence: 0.9, // weighted average of source confidences
lore_verified: true, // set by lore-extractor; false = provisional
// Temporal
created_at: "2026-06-15T10:00:00Z", // wall-clock ingest time
in_fiction_existed: "3rd_age.year_300..3rd_age.year_360", // for Person/Faction/Item
// Provenance
extracted_by: "lore-extractor-1",
extraction_run_id: "uuid-..."
}
lore_verified: false is a critical flag. The LLM is told to treat those nodes as provisional, and there's a dedicated get_unresolved tool (built in slice 2) that lists them.
Constraint strategy
We do not use uniqueness constraints on name for everything — same names can collide across types (Vyr the Faction, Vyr the Person). Instead:
Person.nameunique within:PersonFaction.nameunique within:Faction- Same for every typed label
- A separate
(e:Entity {canonical_id: "uuid"})hub node can be created when a name needs to span types — the engine uses this to register ambiguous references and resolve them.
For the full Cypher that creates the schema, see 08-architecture.md#schema-bootstrap. Cognee manages its own base schema; our typed constraints and indexes live alongside it.
What the ontology deliberately leaves out
- Multi-world / planar. Planes are first-class graph nodes (v1.2, per
17-planes.md). EveryPlanebelongs to aSetting. Every entity hasEXISTS_INedges to its planes (timeless) and time-boundedLOCATED_INedges for "where it was at T." The world-builder uses planes only if the world actually has them; a single-setting, single-plane world is the simplest valid case. - Abstractions like "the Will of the People" or "Destiny." These are not nodes unless the world has a document that treats them as entities. Otherwise the LLM can claim them and the engine has nothing to verify against.
- In-world physics rules. "Can a fireball ignite a dragon?" is not in this schema. It's a LLM-narrative question. The engine tells you what the world's books say about fireballs and dragons; it does not compute the outcome.
The schema is the floor — the minimum that makes the world reason-able. It is not the ceiling. New labels and edges are added as the world demands them, never speculatively.