SGJM / src /sgjm /graph /manager.py
adampippert's picture
SGJM 2026.6.5 — code/docs
e51ccda verified
Raw
History Blame Contribute Delete
5.81 kB
from __future__ import annotations
from collections import defaultdict
from dataclasses import dataclass, field
from typing import Iterable, Iterator, Sequence
from sgjm.graph.address import Address, AddressBook, Signature
from sgjm.graph.node import Node, NodeStatus
@dataclass
class GraphManager:
address_book: AddressBook = field(default_factory=AddressBook)
_nodes: dict[Address, Node] = field(default_factory=dict)
_children: dict[Address, set[Address]] = field(default_factory=lambda: defaultdict(set))
_root: Address | None = None
def __len__(self) -> int:
return len(self._nodes)
def root(self) -> Node | None:
return self._nodes.get(self._root) if self._root is not None else None
def get(self, addr: Address) -> Node:
return self._nodes[addr]
def children(self, addr: Address) -> tuple[Node, ...]:
return tuple(self._nodes[c] for c in self._children.get(addr, ()))
def parents(self, addr: Address) -> tuple[Node, ...]:
return tuple(self._nodes[p] for p in self._nodes[addr].parents)
def frontier(self) -> tuple[Node, ...]:
leaves = []
for addr, node in self._nodes.items():
if node.status in (NodeStatus.REJECTED, NodeStatus.MERGED):
continue
if not self._children.get(addr):
leaves.append(node)
return tuple(leaves)
def add_root(
self,
tokens: Sequence[int],
latent: Sequence[float] = (),
signature: Signature | None = None,
) -> Node:
if self._root is not None:
raise RuntimeError("graph already has a root")
sig = signature or self._signature_for(tokens, latent)
addr, _ = self.address_book.resolve_or_allocate(sig)
node = Node(
address=addr,
parents=(),
tokens=tuple(tokens),
signature=sig,
latent=tuple(latent),
status=NodeStatus.COMMITTED,
depth=0,
)
self._nodes[addr] = node
self._root = addr
return node
def add_child(
self,
parent: Address,
tokens: Sequence[int],
latent: Sequence[float] = (),
score: float = 0.0,
signature: Signature | None = None,
) -> tuple[Node, bool]:
# Returns (node, fresh). When fresh is False, the candidate signature
# collided with an existing node and the parent edge was added instead.
if parent not in self._nodes:
raise KeyError(f"unknown parent {parent}")
sig = signature or self._signature_for(tokens, latent)
addr, fresh = self.address_book.resolve_or_allocate(sig)
if not fresh and addr in self._nodes:
existing = self._nodes[addr]
if parent not in existing.parents:
merged_parents = existing.parents + (parent,)
self._nodes[addr] = Node(
address=existing.address,
parents=merged_parents,
tokens=existing.tokens,
signature=existing.signature,
latent=existing.latent,
status=existing.status,
score=max(existing.score, score),
depth=existing.depth,
metadata=existing.metadata,
)
self._children[parent].add(addr)
return self._nodes[addr], False
depth = self._nodes[parent].depth + 1
node = Node(
address=addr,
parents=(parent,),
tokens=tuple(tokens),
signature=sig,
latent=tuple(latent),
status=NodeStatus.DRAFT,
score=score,
depth=depth,
)
self._nodes[addr] = node
self._children[parent].add(addr)
return node, True
def set_status(self, addr: Address, status: NodeStatus) -> None:
node = self._nodes[addr]
self._nodes[addr] = Node(
address=node.address,
parents=node.parents,
tokens=node.tokens,
signature=node.signature,
latent=node.latent,
status=status,
score=node.score,
depth=node.depth,
metadata=node.metadata,
)
def merge_into(self, src: Address, dst: Address) -> None:
if src == dst:
return
for grand in self._children.pop(src, set()):
grand_node = self._nodes[grand]
new_parents = tuple(p if p != src else dst for p in grand_node.parents)
self._nodes[grand] = Node(
address=grand_node.address,
parents=new_parents,
tokens=grand_node.tokens,
signature=grand_node.signature,
latent=grand_node.latent,
status=grand_node.status,
score=grand_node.score,
depth=grand_node.depth,
metadata=grand_node.metadata,
)
self._children[dst].add(grand)
for kids in self._children.values():
kids.discard(src)
self.set_status(src, NodeStatus.MERGED)
def walk(self, start: Address | None = None) -> Iterator[Node]:
seen: set[Address] = set()
stack = [start if start is not None else self._root]
while stack:
cur = stack.pop()
if cur is None or cur in seen:
continue
seen.add(cur)
yield self._nodes[cur]
stack.extend(self._children.get(cur, ()))
def _signature_for(self, tokens: Sequence[int], latent: Sequence[float]) -> Signature:
if latent:
return Signature.from_latent(latent)
return Signature.from_tokens(tokens)