# 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. ```cypher (: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. ```cypher (: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. ```cypher (: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?" ```cypher (: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. ```cypher (: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.) ```cypher // 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?" ```cypher 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?" ```cypher 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?" ```cypher 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?" ```cypher 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.