tangl.core topology

Graph topology primitives used by the story and vm layers.

Related design docs

Related notes

Topology

class Graph(_ctx=None, *, uid=<factory>, label=None, tags=<factory>, templ_hash=None, members=<factory>, factory=None)[source]

Specialized registry for graph topology.

Why

Graph centralizes topology shape concerns:

  • typed creation helpers for nodes/edges/subgraphs,

  • typed selector-based queries,

  • and graph-level hook bridges for link/unlink operations.

Notes

Creation helpers instantiate kind(**attrs) directly, then rely on registry ownership for binding and lifecycle hooks.

Graph may also carry an optional singleton factory authority. This is explicitly structured and unstructured by hand so graphs can recover behavior registries after persistence. This mirrors the story-layer StoryGraph.world round-trip shim and is an interim pattern until core gains generic recursive handling for entity-shaped fields.

Typed find helpers apply narrowing via selector.with_criteria(has_kind=...). Because with_criteria avoids widening has_kind, callers may narrow kinds but cannot widen helper defaults.

API

  • bind_factory() attaches or clears the singleton graph authority.

  • path_dist() ranks candidates by shared containment ancestry.

  • unstructure() and structure() preserve singleton factory identity across graph round-trips.

Example

>>> g = Graph()
>>> a = Node(label="a", registry=g)
>>> b = Node(label="b", registry=g)
>>> e = g.add_edge(a, b, label="ab")
>>> g.nodes
[<Node:a>, <Node:b>]
>>> e.predecessor is a and e.successor is b
True
class GraphItem(registry=None, graph=None, *, uid=<factory>, label=None, tags=<factory>, templ_hash=None)[source]

Base class for items managed by Graph.

Graph items are registry-aware entities. graph is the preferred alias for the bound registry pointer when the owner is a graph.

class Node(registry=None, graph=None, *, uid=<factory>, label=None, tags=<factory>, templ_hash=None)[source]

Graph vertex with directional edge navigation and wiring helpers.

class Edge(registry=None, graph=None, *, uid=<factory>, label=None, tags=<factory>, templ_hash=None, predecessor_id=None, successor_id=None)[source]

Directed connection between predecessor and successor graph items.

Notes

  • Endpoints may be dangling (None) by design.

Access patterns: - edge.predecessor / edge.successor for graph-dereferenced properties. - edge.set_predecessor(node, _ctx=...) for explicit context-driven mutation. - edge.predecessor = node for ambient-context mutation only.

Example

>>> g = Graph()
>>> n = Node(label="n", registry=g)
>>> e = Edge(registry=g, successor_id=n.uid)
>>> g.find_one(Selector(successor=n))
<Edge:anon->n>
class Subgraph(registry=None, graph=None, *, uid=<factory>, label=None, tags=<factory>, templ_hash=None, member_ids=<factory>)[source]

Non-hierarchical grouping of graph items.

Subgraph reuses EntityGroup membership semantics and adds graph-level link/unlink hook bridge calls when context is provided.

class HierarchicalNode(registry=None, graph=None, *, uid=<factory>, label=None, tags=<factory>, templ_hash=None, member_ids=<factory>)[source]

Node + hierarchy composition.

This class combines HierarchicalGroup (parent/path/ancestor semantics) with Node (edge navigation and wiring helpers).

class Token(registry=None, graph=None, *, uid=<factory>, label=None, tags=<factory>, templ_hash=None, token_from)[source]

Dynamic node wrapper around one singleton instance.

Why

Tokens let frozen Singleton definitions participate in mutable topology. The singleton stores concept-level defaults, while each token stores node-local state.

Key Features

  • Split identity: token_from references the singleton label; label names the token node itself.

  • Delegation + override: reads check token fields first, then delegate missing attributes/methods to reference_singleton.

  • Local instance vars: singleton fields marked json_schema_extra={"instance_var": True} are materialized as mutable token fields on the generated wrapper class.

  • Wrapper cache: repeated Token[SomeType] subscriptions reuse the same cached dynamic class keyed by (Token, SomeType).

API

  • wrapped_cls – singleton type bound to the dynamic token wrapper.

  • token_from – singleton label to resolve within wrapped_cls.

  • label – graph-node name inherited from Node.

  • reference_singleton – access the underlying instance.

  • _instance_vars() – collect instance-var field definitions from the wrapped class.

  • _create_wrapper_cls() – emit a new wrapper subclass with those fields.

Notes

Writes are intentionally asymmetric: instance-var fields are writable on the token, while delegated singleton fields remain frozen and cannot be reassigned through the token.

Examples

>>> class SwordType(Singleton):
...    damage: str
...    sharpness: float = Field(1.0, json_schema_extra={"instance_var": True})
...    def __repr__(self):
...        return ( f"<{self.__class__.__name__}:{self.get_label()}"
...                 f"(damage={self.damage}, sharpness={self.sharpness})>" )
>>> SwordType(label="short sword", damage="1d6")
<SwordType:short sword(damage=1d6, sharpness=1.0)>
>>> t = Token[SwordType](token_from="short sword",
...                      label="Glamdring", sharpness=2.0); t
<Token[...SwordType]:Glamdring(damage=1d6, sharpness=2.0)>
>>> t.has_kind(SwordType)
True
>>> t.sharpness -= 0.5; t  # used it, mutate instance var
<Token[...SwordType]:Glamdring(damage=1d6, sharpness=1.5)>
>>> SwordType.get_instance("short sword").sharpness  # reference unchanged
1.0