Skip to content

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)--> concept from projected P2 edges
  • concept --measured_by/filtered_by--> asset from S1 bindings
  • asset <--joins_via--> asset from 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.