Files
lore-engine/docs/17-planes.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

19 KiB

17 — Planes of Existence: A First-Class Graph Model

Status: v1.2 amendment (replaces v1.1's flat world_id strings with graph nodes). Drives: Q1, Q2, Q3 from the design review (planes as first-class nodes; "worlds are planes"; EXISTS_IN is a timeless type-assertion; the four plane-relation edge types).

This document is the new plane model. It supersedes the v1.1 "world_id string namespace" pattern. The change is motivated by the limitations of the flat-string model when faced with D&D-style cosmology: a string can name a plane, but it cannot model the relations between planes (Shadowfell reflects Material, Astral is a transit layer, the Nine Hells has nine sub-layers).

After this amendment:

  • Plane is a first-class graph node.
  • A Setting is a parent that owns its planes (via the setting_id field on each Plane).
  • Every entity has zero or more EXISTS_IN edges to Setting nodes (timeless type-assertion — the slice 6.5 setting filter consumes this edge).
  • Every entity has zero or more time-bounded LOCATED_IN_PLANE edges to Plane nodes (where it was at time T).
  • Planes have relations to other planes: REFLECTS, LAYER_OF, ADJACENT_TO, ACCESSIBLE_VIA.
  • The old world_id string property is deprecated but kept for read-only compatibility during the migration window.

The key insight: "worlds are planes"

In v1.1 we used world_id as a flat string on every node. The values in the seed data were default, arda_greyscale, mardonar, voldramir. The flaw: a "world" was a single string with no internal structure, so it could not represent the relations between worlds.

In v1.2 the model is rewritten: a "world" is just the primary Material Plane of a setting. When a player says "Mardonar," they mean the Material Plane of the Mardonari setting — not a separate universe. The same person can be in Mardonar's Material Plane today, in Mardonar's Voldramir demiplane next week, and in the Mardonari setting's Astral transit layer in between. The plane changes; the person does not get duplicated.

This is the D&D-canonical model. It also happens to be what world_id was trying to express all along, just with the wrong abstraction.

Node labels (new)

Setting

A campaign, game, or world scope. The top of the world hierarchy. A Setting is the unit of "what world are we talking about" — Eberron, the Forgotten Realms, Mardonari, the Iron Kingdoms, a home-brew demiplane-only setting.

Property Type Notes
id string Stable slug, e.g. eberron, mardonari, default (legacy alias for the v1 default world).
name string Human-readable, e.g. "Eberron," "Mardonari."
summary string One-paragraph world description.
genres string[] e.g. ["high_fantasy","steampunk","noir"].
canonical_time string The current in-fiction time of the campaign. Format: {era}.{unit} (see 02-time-model.md).

A Setting is the root of the plane tree. Every Plane belongs to exactly one Setting via the setting_id field on the Plane node (a deliberate simplification — see the "Edge types" section). A Setting with no planes is valid (and represents a world whose plane structure is not yet detailed).

Plane

A layer of existence within or alongside a setting. The engine treats all planes uniformly — there is no special-cased Material plane. Planes are typed by their kind property, and relations between planes are first-class edges.

Property Type Notes
id string Stable slug within the setting, e.g. eberron.material, mardonari.voldramir, default.arda_greyscale.
name string Human-readable, e.g. "The Material Plane," "Voldramir," "Mount Celestia."
kind string enum See the taxonomy below.
summary string Optional one-paragraph description.
parent_plane string (Plane.id) For layered planes (Nine Hells → Dis → Minauros, etc.).
accessible boolean Whether mortals can reach the plane without artifact/spell assistance. False for the Far Realm, true for Material.
alignment_tendency string Optional, e.g. lawful_good, chaotic_evil, neutral.
valid_from string When this plane came into existence. Format: {era}.{unit}.
valid_until string When it ceased to exist. null for still-active planes.

The plane taxonomy (the kind enum)

The engine does not hard-code any specific planes. kind is a free-form string, but the recommended taxonomy is the D&D-style cosmology:

kind Meaning Examples Notes
material The primary, baseline plane. The world players normally inhabit. Eberron Material, Mardonari Material, Forgotten Realms Material. Every setting has one. The "world" colloquially.
reflection A plane that mirrors the material plane but with different qualities. Shadowfell (dark), Feywild (vibrant), the parallel Material of the Arda greyscale seed. Has a REFLECTS edge to a material plane.
transit A plane used to travel between other planes. Astral Plane, Ethereal Plane. Has ADJACENT_TO edges to other planes.
ethereal A plane that overlaps the material — you can see into both. The Ethereal Plane. Distinct from transit because the overlap is geometric, not journey-based.
outer A plane inhabited by deities, fiends, celestials, or other extraplanar beings. Mount Celestia, the Nine Hells, the Abyss, Mechanus, the Beastlands, Gehenna. Grouped by alignment tendency.
inner A plane embodying an elemental or quasi-elemental force. Plane of Fire, Plane of Water, Plane of Earth, Plane of Air, Para-elemental planes. Often paired.
demiplane A small, finite, often personalized plane. A wizard's pocket dimension, Voldramir, the demiplane created by Mordenkainen's Magnificent Mansion, Ravenloft (a demiplane that imprisons). Has a contained_in (often Material) and is finite in size.
transcendent A plane that exists outside the standard cosmology. The Far Realm (lovecraftian outer), the realm of the gods-as-concept (in settings where "the gods" is a place, not beings). Reserved for cosmology-bending settings.

A setting does not need every kind. A low-magic home-brew world might have only material + a single demiplane. A planes-heavy D&D setting has dozens.

Edge types (plane model)

Setting ↔ Plane membership (Plane.setting_id field)

Every Plane belongs to exactly one Setting. The engine encodes this as a setting_id field on the Plane node (rather than as a HAS_PLANE edge) because the relation is single-valued and 1-to-many: one Setting has many Planes, but each Plane has exactly one parent. The reverse lookup planes_in_setting(setting_id) is O(1) via a backend index.

The conceptual model — "Setting is the root of the plane tree" — is unchanged; only the storage form differs from the pure-Cypher shape shown in earlier drafts. The pattern (:Setting {id: 'eberron'})-[:HAS_PLANE]->(:Plane {id: 'eberron.material'}) is the intent; the implementation is (:Plane {id: 'eberron.material', setting_id: 'eberron', kind: 'material'}) plus the reverse index.

EXISTS_IN (any entity → Setting) — timeless type-assertion

This edge asserts that the entity type exists in the setting. It is not time-bounded. It answers "is this entity a member of this setting?" — the cross-setting filter used by the slice 6.5 read tools.

(:Person {name: 'Roland Raventhorne'})-[:EXISTS_IN]->(:Setting {id: 'mardonari'})
(:Person {name: 'The Wanderer'})-[:EXISTS_IN]->(:Setting {id: 'the_wild_dream'})

EXISTS_IN (entity → Setting) is many-to-many. A plane-transcendent god can belong to multiple settings. A mortal typically belongs to one. The relation is the slice 6.5 setting filter's primary mechanism.

The setting → plane direction is not EXISTS_IN; it is the setting_id field on the Plane. The two relations have different shapes (an entity can exist in many settings; a plane belongs to exactly one setting), so the engine models them differently.

For the v1.1 migration story (the EXISTS_IN of an entity → plane in v1.1 design), the engine separates "setting membership" (the timeless type-assertion) from "plane presence" (where the entity was at time T). See LOCATED_IN_PLANE below.

LOCATED_IN_PLANE (any entity → Plane) — time-bounded

This edge says "the entity was at this plane at time T." It is time-bounded via valid_from and valid_until, exactly like the other time-bounded relations in the engine. It answers "where was Roland in year 312 TA?" not "what kind of plane is Roland from?"

REFLECTS (Plane → Plane) — material mirrors

A plane that is a reflection of another. Shadowfell reflects Material, Feywild reflects Material, the Arda greyscale "world" reflects Eberron's Material. A reflection is a parallel universe that mirrors the geography of its source but with twisted or inverted qualities.

(:Plane {id: 'eberron.shadowfell', kind: 'reflection'})-[:REFLECTS]->(:Plane {id: 'eberron.material'})
(:Plane {id: 'eberron.feywild', kind: 'reflection'})-[:REFLECTS]->(:Plane {id: 'eberron.material'})

This is directed. Eberron's Shadowfell reflects Eberron's Material, not Forgotten Realms' Material. The plane-id is the disambiguator.

LAYER_OF (Plane → Plane) — sub-planes

A plane that is a sub-layer of a larger plane. The Nine Hells is a single Outer Plane, but it has nine layers (Avernus, Dis, Minauros, Phlegethos, Stygia, Malbolge, Maladomini, Cania, Nessus). A layer LAYER_OF its parent plane. This is also how a demiplane LAYER_OF its containing plane if you prefer that model.

(:Plane {id: 'eberron.dis', kind: 'outer'})-[:LAYER_OF]->(:Plane {id: 'eberron.nine_hells', kind: 'outer'})
(:Plane {id: 'mardonari.voldramir', kind: 'demiplane'})-[:LAYER_OF]->(:Plane {id: 'mardonari.material', kind: 'material'})

A plane can have many layers. A layer can be a parent of further layers (Dis has sub-layers in some cosmologies). The relation is not required — a flat taxonomy is valid.

ADJACENT_TO (Plane → Plane) — geometric neighbors

A plane that is geometrically adjacent — you can physically reach it by walking, climbing, swimming, or by being there at the right moment. Material is adjacent to Ethereal (the Ethereal Plane overlaps it). Material is adjacent to the Astral in some cosmologies. Adjacency is the answer to "can I walk there from where I am?"

(:Plane {id: 'eberron.material'})-[:ADJACENT_TO]->(:Plane {id: 'eberron.ethereal', kind: 'ethereal'})
(:Plane {id: 'eberron.material'})-[:ADJACENT_TO]->(:Plane {id: 'eberron.astral', kind: 'transit'})

ADJACENT_TO is symmetric. If Material is adjacent to Ethereal, Ethereal is adjacent to Material. The Cypher pattern uses undirected traversal.

ACCESSIBLE_VIA (Plane → Spell OR Plane → Item OR Plane → Location)

A plane that is reachable by a specific mechanism. The most common form is spell: Material is ACCESSIBLE_VIA Plane Shift, Astral Projection, or Gate. It is also valid to point at a physical portal: the Nine Hells is ACCESSIBLE_VIA the Stygian Gate in Dis.

(:Plane {id: 'eberron.nine_hells'})-[:ACCESSIBLE_VIA]->(:Spell {name: 'Plane Shift'})
(:Plane {id: 'eberron.material'})-[:ACCESSIBLE_VIA]->(:Location {name: 'The Stygian Gate'})

This is the "is there a way to get there" relation. It is also the LLM-friendly one: "I'm at X, can I get to Y?" becomes a graph traversal.

The canonical plane taxonomy (a starter set for D&D-style settings)

This is a recommended starting set, not a hard requirement. The engine does not require any of these planes to exist.

Plane kind reflects adjacent_to layer_of notes
{setting}.material material ethereal, astral Required.
{setting}.shadowfell reflection material ethereal Optional. Dark mirror.
{setting}.feywild reflection material Optional. Vibrant mirror.
{setting}.ethereal ethereal material, shadowfell Overlaps material.
{setting}.astral transit material, outer planes Transit layer.
{setting}.mount_celestia outer astral Lawful good afterlife.
{setting}.nine_hells outer astral Lawful evil afterlife.
{setting}.abyss outer astral Chaotic evil.
{setting}.mechanus outer astral Lawful neutral.
{setting}.beastlands outer astral Chaotic neutral.
{setting}.far_realm transcendent Lovecraftian outer.

The {setting} prefix is a convention, not a constraint. A setting with one plane can use the bare name (material instead of eberron.material).

Cypher patterns

"What planes does this entity exist in?"

The timeless version: an entity's setting membership + all planes in that setting. (For v1.2, the direct entity→plane relation is time-bounded LOCATED_IN_PLANE, not the timeless one — the timeless one is the setting filter.)

// What setting(s) does Asmodeus belong to?
MATCH (p:Person {name: 'Asmodeus'})-[:EXISTS_IN]->(s:Setting)
RETURN s.id

// What planes are in that setting?
MATCH (plane:Plane)
WHERE plane.setting_id IN ['mardonari']
RETURN plane.id, plane.kind, plane.name

"Where was this entity at time T?"

MATCH (p:Person {name: 'Roland Raventhorne'})-[r:LOCATED_IN_PLANE]->(plane:Plane)
WHERE time_in_window('3rd_age.year_430', r.valid_from, r.valid_until)
RETURN plane.id, plane.kind, plane.name

"What planes are accessible from where I am?"

MATCH (me)-[:LOCATED_IN_PLANE {valid_until: null}]->(here:Plane)
MATCH (here)-[:ADJACENT_TO|ACCESSIBLE_VIA*1..2]->(target:Plane)
WHERE target <> here
RETURN DISTINCT target.id, target.kind, target.name

"What is the demiplane of Voldramir, and what contains it?"

MATCH (d:Plane {id: 'mardonari.voldramir'})-[:LAYER_OF]->(parent:Plane)
RETURN d.name AS demiplane, parent.name AS contained_in

"What reflections of Material exist in this setting?"

MATCH (material:Plane {kind: 'material'})<-[:REFLECTS]-(reflection:Plane)
WHERE material.id STARTS WITH 'eberron.'
RETURN reflection.id, reflection.kind

Migration from v1.1

The v1.1 design used world_id: string on every node as a flat namespace. The v1.2 model replaces that with the EXISTS_IN graph traversal. The migration is:

  1. Add Setting and Plane nodes for every distinct world_id value currently in the graph. default becomes Setting("default") + Plane("default.material", kind: 'material'). mardonar becomes Plane("mardonari.material", kind: 'material') under Setting("mardonari"). voldramir becomes Plane("mardonari.voldramir", kind: 'demiplane') under Setting("mardonari"). arda_greyscale becomes Plane("default.arda_greyscale", kind: 'demiplane') under Setting("default").

  2. Create EXISTS_IN edges from every entity to its primary Setting (not plane). For most nodes this is a single edge. For cross-setting entities (gods, outsiders), it is many. The setting→plane direction is encoded as a setting_id field on each Plane; no separate edge is needed.

  3. Mark the legacy world_id property deprecated but do not delete it. Plugin queries check for EXISTS_IN first, fall back to world_id if the new edges are not present (during the migration window).

  4. Update plugins and the seed to use the new model. The seed script writes the planes; the plugins query by plane.

  5. Remove world_id property in v2.0. After the migration is stable, the string property is dropped. Plugin queries that still reference it are bugs.

  6. Rename the Postgres world table to setting. The v1.1 Postgres schema had a world table and a world_id foreign key on operational tables (lore_event, trade_log, retcon, dialogue_log). Per v1.2, this becomes the setting table with a setting_id foreign key; add a kind enum column (single_plane | multi_plane) to mirror the v1.2 Setting.kind field. See 12-storage-strategy.md.

  7. Update YAML instance files. v1.1 YAML ingest used a flat world_id: "arda_1st_age" string at the top of each file. v1.2 YAML uses setting_id: "mardonari" (or whichever Setting the instance belongs to). The structured EXISTS_IN edge is generated from the setting_id field at ingest time, pointing to the Setting's primary Material Plane. See 11-extensibility.md and 14-examples.md for the renamed YAML schema.

Open questions and trade-offs

EXISTS_IN (entity → Setting) is many-to-many

Default choice (Q-clarify default): a node can have EXISTS_IN edges to many settings. The trade-off:

  • Many-to-many (default). A god exists in their home setting, the Astral (transit), and possibly Material (mortal world). A mortal exists in one setting but can be extended to many. Most flexible, but means a "what settings is this entity in" query can return many rows and the LLM has to disambiguate.
  • One-to-one. Each entity has a single primary setting. If it also exists in others, that is a LOCATED_IN_PLANE edge (time-bounded). More restrictive, matches D&D "type" thinking ("Aldric is a Material-Plane creature" → Aldric is in the Material setting).
  • One or more, but at least one. Same as many-to-many, but missing EXISTS_IN is a consistency violation rather than a provisional state.

The default is reversible — it is a Cypher pattern, not a schema migration. If a downstream user (the LLM, the world-builder) finds many-to-many cluttered, switching to one-to-one is a refactor of the seed and a small plugin change. The design note here is: when in doubt, support the more general case and let the LLM narrow.

Layer-of vs contained-in

A demiplane is contained in a plane (Voldramir is contained in Mardonari's Material). The taxonomy uses LAYER_OF for both "sub-layer" (Dis is a layer of the Nine Hells) and "contained in" (Voldramir is a layer of Mardonari's Material). An alternative is to add a CONTAINED_IN edge, but the dual semantics of LAYER_OF ("a sub-plane") cover both. The Cypher remains the same.

Plane relations that aren't in this doc

If the world needs more plane-edge types (e.g., BORDERS, TRADE_ROUTE, WAR_WITH), they are added to the ontology the same way other edge types are. The four in Q4 (REFLECTS, LAYER_OF, ADJACENT_TO, ACCESSIBLE_VIA) are the recommended starting set.

What this is not

  • Not a renaming of world_id to plane_id. The new model is a graph of planes with relations, not a string namespace. The migration is structural, not cosmetic.
  • Not D&D-specific. The taxonomy is D&D-flavored because the engine's use case is high fantasy, but the model works for any cosmology: the Wheel of Time has its own pattern (the Pattern, the One Power, the Dark One's prison — a transcendent plane), Malazan has its warrens, Cosmere has its three realms. The taxonomy is a recommendation, not a constraint.
  • Not finished. The taxonomy will grow as the engine is used. The kind enum, the relation types, the time-boundedness of EXISTS_IN (currently timeless), the migration timeline — all of these are open to revision in v2 based on what the LLM and the world-builder actually do with the model.