slice 6.3: plane-relation edge type characterisation tests

Pins the contract for REFLECTS, LAYER_OF, ADJACENT_TO,
ACCESSIBLE_VIA: each is a typed, timeless Edge (Layer 1), NOT a
reified :Relation node (Layer 2). Per docs/17-planes.md, plane
relations are structural — they don't carry per-entity time
bounds; reified :Relation is reserved for time-bounded facts
(slice 6.5).

Tests cover round-trip through InMemoryGraph's subject / object
/ id indexes, timelessness (valid_from/valid_until both None),
and the non-reified property (no :Relation node created on add).
The 4 edge names are pinned in EDGE_TYPES per slice 6.1.

+6 tests (726 → 732). All green. No regressions.
This commit is contained in:
Lore Engine Dev
2026-06-19 13:04:56 -04:00
parent 5a8bf67afb
commit 609db79a54

View File

@@ -0,0 +1,260 @@
"""Slice 6.3 — Plane-relation edge types: typed, non-reified, timeless.
Per ``docs/plan/exec/06-planes.md`` sub-slice 6.3:
- ``REFLECTS``, ``LAYER_OF``, ``ADJACENT_TO``, ``ACCESSIBLE_VIA``
are **typed edges** (Edge dataclass, ``relation`` field),
**not** reified ``:Relation`` nodes.
- They are **timeless** (no ``valid_from`` / ``valid_until``).
- One round-trip test per edge type, plus cross-cutting
tests asserting the "non-reified" property (no relation
node is created) and the "timeless" property (no time
bounds are stamped).
Why "not reified":
Per ``docs/17-planes.md``, the plane relations
(``REFLECTS``, ``LAYER_OF``, ``ADJACENT_TO``,
``ACCESSIBLE_VIA``) are *structural* facts about the world's
geometry — they don't change over the timeline the
``EXISTS_IN`` / time-bounded :Relation layer tracks. The
reified ``:Relation`` (ADR 0009) exists for time-bounded
facts (e.g. "Roland Raventhorne was MEMBER_OF House
Raventhorne from year 200 to year 300"). A plane reflection
is a property of the planes themselves, not a per-entity
time-bounded fact.
Why "timeless":
These edges are added to ``EDGE_TYPES`` (slice 6.1) without
any ``valid_from`` / ``valid_until``. A retcon that changes
a plane relation is a structural change (the world-builder
rewrites the design doc); it doesn't have a "valid during"
window. If a future slice needs a time-bounded reflection
(e.g. "The Feywild reflected the Material Plane only during
the 2nd Age"), that's a separate, time-bounded :Relation
that the slice 6.5 work enables.
"""
from __future__ import annotations
from lore_engine_poc import ontology
from lore_engine_poc.graph_backend import InMemoryGraph
from lore_engine_poc.tools import Edge
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _plane_edge(relation: str, subject: str, object_: str) -> Edge:
"""Build a plane-relation Edge with explicit no-time-bounds.
The ``valid_from=None`` / ``valid_until=None`` is the
"timeless" property the design doc pins — these edges do
not carry time windows. The Edge dataclass's default
factory leaves both as ``None``, so the explicit kwarg
is documentary, not behavioural; the test still asserts
on the values to catch accidental default-factory
changes.
"""
return Edge(
subject=subject,
relation=relation,
object=object_,
valid_from=None,
valid_until=None,
)
# ---------------------------------------------------------------------------
# Per-edge round-trip tests — one for each typed relation
# ---------------------------------------------------------------------------
def test_6_3_plane_relation_round_trip_reflects() -> None:
"""AC 6.2 — ``REFLECTS`` is a typed, timeless Edge.
Example: the Feywild REFLECTS the Material Plane. Both
endpoints are Plane nodes; the relation is structural,
not per-entity.
"""
g = InMemoryGraph()
edge = _plane_edge("REFLECTS", "mardonari.feywild", "mardonari.material")
g.add(edge)
# Subject-side lookup.
subject_edges = g.edges_for_subject("mardonari.feywild", relation="REFLECTS")
assert len(subject_edges) == 1
assert subject_edges[0].object == "mardonari.material"
# Object-side lookup.
object_edges = g.edges_for_object("mardonari.material", relation="REFLECTS")
assert len(object_edges) == 1
assert object_edges[0].subject == "mardonari.feywild"
# Id-indexed lookup — same Edge object.
found = g.find_edge_by_id(edge.edge_id)
assert found is edge
# Timeless — no time bounds stamped.
assert found.valid_from is None
assert found.valid_until is None
# Non-reified — no :Relation node created.
assert g.find_relation_by_id(edge.edge_id) is None
def test_6_3_plane_relation_round_trip_layer_of() -> None:
"""AC 6.2 — ``LAYER_OF`` connects a Region (or subsidiary
plane) to its parent Plane.
Example: the Underdark is a ``LAYER_OF`` Voldramir (the
shadow plane of Mardonari). This is the canonical
Region→Plane migration fact per
``docs/plan/06-slice-planes.md`` AC 6.8.
"""
g = InMemoryGraph()
edge = _plane_edge("LAYER_OF", "mardonari.underdark", "mardonari.voldramir")
g.add(edge)
subject_edges = g.edges_for_subject("mardonari.underdark", relation="LAYER_OF")
assert len(subject_edges) == 1
assert subject_edges[0].object == "mardonari.voldramir"
object_edges = g.edges_for_object("mardonari.voldramir", relation="LAYER_OF")
assert len(object_edges) == 1
assert object_edges[0].subject == "mardonari.underdark"
found = g.find_edge_by_id(edge.edge_id)
assert found is edge
assert found.valid_from is None
assert found.valid_until is None
assert g.find_relation_by_id(edge.edge_id) is None
def test_6_3_plane_relation_round_trip_adjacent_to() -> None:
"""AC 6.2 — ``ADJACENT_TO`` is a symmetric typed edge.
Example: the Material Plane is ADJACENT_TO the Astral
Plane. Symmetric in the sense that the relation is
unordered (the design doesn't model directional planar
adjacency); a follow-up might add ``ADJACENT_FROM`` /
``ADJACENT_TO`` asymmetry if needed, but for v1.2
adjacency is undirected.
"""
g = InMemoryGraph()
edge = _plane_edge("ADJACENT_TO", "mardonari.material", "mardonari.astral")
g.add(edge)
# Both directions resolve.
assert any(
e.object == "mardonari.astral"
for e in g.edges_for_subject("mardonari.material", relation="ADJACENT_TO")
)
assert any(
e.subject == "mardonari.material"
for e in g.edges_for_object("mardonari.astral", relation="ADJACENT_TO")
)
found = g.find_edge_by_id(edge.edge_id)
assert found is not None
assert found.relation == "ADJACENT_TO"
assert found.valid_from is None
assert found.valid_until is None
assert g.find_relation_by_id(edge.edge_id) is None
def test_6_3_plane_relation_round_trip_accessible_via() -> None:
"""AC 6.2 — ``ACCESSIBLE_VIA`` is a portal-gated typed edge.
Example: the Shadowfell is ACCESSIBLE_VIA the Underdark
(a portal network that connects the two). The relation
is directional in the design — the *target* plane is
reachable *via* the *source* plane's portal.
"""
g = InMemoryGraph()
edge = _plane_edge(
"ACCESSIBLE_VIA",
"mardonari.shadowfell", # the plane being reached
"mardonari.underdark", # the portal source
)
g.add(edge)
subject_edges = g.edges_for_subject(
"mardonari.shadowfell", relation="ACCESSIBLE_VIA"
)
assert len(subject_edges) == 1
assert subject_edges[0].object == "mardonari.underdark"
# The portal side resolves the relation from the object
# index — useful for "what can I reach from this plane?"
# queries that walk the object index in reverse.
object_edges = g.edges_for_object(
"mardonari.underdark", relation="ACCESSIBLE_VIA"
)
assert any(e.subject == "mardonari.shadowfell" for e in object_edges)
found = g.find_edge_by_id(edge.edge_id)
assert found is not None
assert found.valid_from is None
assert found.valid_until is None
assert g.find_relation_by_id(edge.edge_id) is None
# ---------------------------------------------------------------------------
# Cross-cutting properties: non-reified + timeless
# ---------------------------------------------------------------------------
def test_6_3_all_plane_edges_are_in_edge_types() -> None:
"""AC 6.2 — the 4 plane-relation strings are in
``EDGE_TYPES``.
Per slice 6.1, this is the closed vocabulary the
consistency engine validates against. The test pins
the names so a typo in a future slice is caught at
the import level, not at the runtime read-tool
dispatch.
"""
expected = {"REFLECTS", "LAYER_OF", "ADJACENT_TO", "ACCESSIBLE_VIA"}
assert expected <= set(ontology.EDGE_TYPES), (
f"EDGE_TYPES missing plane relations: "
f"{expected - set(ontology.EDGE_TYPES)}"
)
def test_6_3_plane_edges_do_not_create_relation_nodes() -> None:
"""AC 6.2 — plane edges are Layer 1 ``Edge`` records, NOT
Layer 2 reified ``:Relation`` nodes.
Per ``docs/11-extensibility.md``, Layer 2 (:Relation) is
for time-bounded facts about specific entity pairs (e.g.
"Roland was MEMBER_OF House Raventhorne from year 200 to
year 300"). Plane relations are structural — they don't
carry per-entity time bounds. Mixing them in Layer 2
would force the world-builder to reify every plane edge
as a time-bounded membership, which the design explicitly
rejects.
"""
g = InMemoryGraph()
edges = [
_plane_edge("REFLECTS", "p1", "p2"),
_plane_edge("LAYER_OF", "p3", "p1"),
_plane_edge("ADJACENT_TO", "p1", "p4"),
_plane_edge("ACCESSIBLE_VIA", "p4", "p3"),
]
for e in edges:
g.add(e)
# No Layer 2 :Relation nodes — the polymorphic relation
# storage is empty.
assert g.relation_nodes == {}
assert g.relation_nodes_by_endpoint == {}
# The reverse-lookup helpers all return empty.
assert g.relations_for("p1") == []
assert g.relations_for("p1", direction="in") == []
assert g.relations_for("p1", direction="both") == []
# But the Layer 1 Edge indexes are populated.
assert g.find_edge_by_id(edges[0].edge_id) is edges[0]
assert g.find_edge_by_id(edges[3].edge_id) is edges[3]