Graph serving tier
smrititantra.core.graph_store
The graph serving tier: one projection of the knowledge graph, queried.
Recall reads a compiled snapshot tuned for one question. This tier answers the orthogonal question — how does everything connect? — that recall deliberately doesn't: blast-radius ("what breaks if this asset changes?"), lineage ("what does this metric depend on?"), and neighbourhood extraction.
There is exactly one implementation of the surfaces: the graph is projected from the same event-sourced store every other plane reads — concept nodes and their projected edges (P2), data assets (P1), and the bindings that tie a concept to the asset that computes it (S1). The projection is directed so dependency direction is meaningful:
concept --(rel_class)--> conceptfrom projected P2 edgesconcept --measured_by/filtered_by--> assetfrom S1 bindingsasset <--joins_via--> assetfrom S1 join bindings (both directions)
Direction convention: an out-edge points from a dependent to its dependency. So lineage (what X is built from) walks out-edges; impact (what is built on X) walks in-edges.
Deployment is a deliberate commitment (SMRITITANTRA_GRAPH_BACKEND): unset -> the
surfaces fail fast with :class:GraphTierNotDeployedError, leaving recall,
learning, and execution untouched. memory projects straight from the store
(the reference tier, no extra deps); neo4j is the scale persistence for the
same projection.
Every traversal is ACL-trimmed at projection time — a node the principal's clearance does not dominate never enters the graph, so it can neither be returned nor bridge a path between two nodes the principal can see.
GraphResult(root: str, nodes: tuple[GraphNode, ...], edges: tuple[GraphEdge, ...], truncated: bool = False)
dataclass
A traversal result: the reachable nodes and the edges among them.
GraphService(store: MemoryStore, config: SmrititantraConfig, tenant_config: TenantConfig, *, tier: Any = None)
The graph serving tier over one tenant's store (single implementation).
tier is the optional scale persistence (:class:Neo4jGraphStore); when
the configured backend is neo4j and a tier is injected, traversal runs
there — same surface, same gates, same result shape.
impact(tenant: str, principal: Principal, ref: str, *, depth: int = 3) -> GraphResult
async
Downstream blast radius: everything built on ref (walks dependents).
lineage(tenant: str, principal: Principal, ref: str, *, depth: int = 3) -> GraphResult
async
Upstream: everything ref is built from (walks dependencies).
query(tenant: str, principal: Principal, start: str, *, direction: str = 'both', depth: int = 2, edge_types: tuple[str, ...] | None = None, limit: int = 200) -> GraphResult
async
A depth- and size-capped traversal from start in direction.
subgraph(tenant: str, principal: Principal, ref: str, *, depth: int = 1) -> GraphResult
async
The undirected neighbourhood of ref to depth hops.
Neo4jGraphStore(uri: str, user: str | None = None, password: str | None = None, *, driver: Any = None)
The scale persistence for the graph tier ([neo4j] extra).
Projected incrementally from the ledger: project() reads
the p1.*/p2.*/s1.* events since the last projected position
(kept on a :ProjectionState node in the graph itself, so the
projection is self-describing), re-reads the affected rows from the system
of record, and MERGEs exactly those — rebuild=True wipes the tenant
subgraph and projects everything (the recovery path). An event that does
not identify its entity falls back to a full pass rather than guessing.
Traversal enforces visibility in the path predicate — every node on a matched path must carry a sensitivity the caller's clearance dominates, so a hidden node can neither be returned nor bridge two visible ones (the same no-bridge guarantee the in-memory tier gives by construction).
Every statement goes through _run so tests can drive the store with a
fake runner; the live leg runs under the neo4j marker.
ProjectionReport(from_seq: int, to_seq: int, full: bool = False, nodes_written: int = 0, edges_written: int = 0, nodes_deleted: int = 0)
dataclass
What one projection pass did (and from/to which ledger position).
extract_graph(store: MemoryStore, tenant: str, tenant_config: TenantConfig) -> tuple[dict[str, GraphNode], list[GraphEdge]]
async
The shared projection content, untrimmed: every backend (the in-memory tier, the Neo4j tier) writes exactly this node/edge set — single-implementation of the mapping. Visibility is applied where each backend serves (memory: at projection per principal; Neo4j: as a path predicate at query time).
graph_tier_from_config(config: SmrititantraConfig) -> Neo4jGraphStore | None
The configured scale tier, or None when serving from the store.