Files
lore-engine/docs/01-ontology.md
Kaysser Kayyali 122ce88295 slice 6 docs cleanup + ADR 0013
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>
2026-06-19 20:44:04 -04:00

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

  1. 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.
  2. 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.
  3. 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).
  4. Sources are first-class. Every claim traces back to a LoreSource. The LLM is told which document said what.
  5. 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.
  6. 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 DataPoint base class and its own chunking/embedding — that is what's genuinely inherited. The labels below are Lore Engine types built on top of DataPoint, not Cognee primitives. In particular, Contradiction (and the other consistency nodes) are built from scratch in slice 2 — Cognee ships no contradiction machinery. Message/Topic are 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 Person node is the canonical entity. NPCs and PCs both reference it.
  • NPC tier scoping (existing v1) works on the Person node. query_as_npc resolves to the Person via the NPC wrapper.
  • 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 Human or PC nodes are created. The NPC and Person nodes 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 AgeAge 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_id field on the Plane node, not as a HAS_PLANE edge. This is a simplification — every Plane has exactly one Setting, so a single field is sufficient. The reverse lookup planes_in_setting(setting_id) is O(1) via the planes_by_setting index. The Cypher pattern from 17-planes.md is 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.name unique within :Person
  • Faction.name unique 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). Every Plane belongs to a Setting. Every entity has EXISTS_IN edges to its planes (timeless) and time-bounded LOCATED_IN edges 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.